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

SAP / sap-btp-service-operator / 16957532736

14 Aug 2025 06:20AM UTC coverage: 79.864% (-0.09%) from 79.955%
16957532736

Pull #538

github

web-flow
Merge branch 'main' into transient-error-optimization
Pull Request #538: transient error - prototype

5 of 17 new or added lines in 4 files covered. (29.41%)

6 existing lines in 1 file now uncovered.

2824 of 3536 relevant lines covered (79.86%)

0.9 hits per line

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

82.09
/controllers/serviceinstance_controller.go
1
/*
2

3

4
Licensed under the Apache License, Version 2.0 (the "License");
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    http://www.apache.org/licenses/LICENSE-2.0
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
package controllers
18

19
import (
20
        "context"
21
        "encoding/json"
22
        "fmt"
23
        "net/http"
24
        "strings"
25

26
        "github.com/pkg/errors"
27
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
28

29
        "k8s.io/apimachinery/pkg/types"
30

31
        "sigs.k8s.io/controller-runtime/pkg/predicate"
32

33
        "github.com/SAP/sap-btp-service-operator/api/common"
34
        "github.com/SAP/sap-btp-service-operator/internal/config"
35
        "github.com/SAP/sap-btp-service-operator/internal/utils"
36
        "github.com/go-logr/logr"
37
        "k8s.io/apimachinery/pkg/runtime"
38
        "k8s.io/client-go/tools/record"
39

40
        "k8s.io/client-go/util/workqueue"
41
        "sigs.k8s.io/controller-runtime/pkg/controller"
42

43
        v1 "github.com/SAP/sap-btp-service-operator/api/v1"
44
        "k8s.io/apimachinery/pkg/api/meta"
45

46
        "github.com/google/uuid"
47

48
        "github.com/SAP/sap-btp-service-operator/client/sm"
49
        smClientTypes "github.com/SAP/sap-btp-service-operator/client/sm/types"
50
        apierrors "k8s.io/apimachinery/pkg/api/errors"
51
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
52
        ctrl "sigs.k8s.io/controller-runtime"
53
        "sigs.k8s.io/controller-runtime/pkg/client"
54
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
55
)
56

57
// ServiceInstanceReconciler reconciles a ServiceInstance object
58
type ServiceInstanceReconciler struct {
59
        client.Client
60
        Log         logr.Logger
61
        Scheme      *runtime.Scheme
62
        GetSMClient func(ctx context.Context, serviceInstance *v1.ServiceInstance) (sm.Client, error)
63
        Config      config.Config
64
        Recorder    record.EventRecorder
65
}
66

67
// +kubebuilder:rbac:groups=services.cloud.sap.com,resources=serviceinstances,verbs=get;list;watch;create;update;patch;delete
68
// +kubebuilder:rbac:groups=services.cloud.sap.com,resources=serviceinstances/status,verbs=get;update;patch
69
// +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch;delete
70
// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;create;update
71

72
func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
1✔
73
        log := r.Log.WithValues("serviceinstance", req.NamespacedName).WithValues("correlation_id", uuid.New().String())
1✔
74
        ctx = context.WithValue(ctx, utils.LogKey{}, log)
1✔
75

1✔
76
        serviceInstance := &v1.ServiceInstance{}
1✔
77
        if err := r.Client.Get(ctx, req.NamespacedName, serviceInstance); err != nil {
2✔
78
                if !apierrors.IsNotFound(err) {
1✔
79
                        log.Error(err, "unable to fetch ServiceInstance")
×
80
                }
×
81
                // we'll ignore not-found errors, since they can't be fixed by an immediate
82
                // requeue (we'll need to wait for a new notification), and we can get them
83
                // on deleted requests.
84
                return ctrl.Result{}, client.IgnoreNotFound(err)
1✔
85
        }
86
        serviceInstance = serviceInstance.DeepCopy()
1✔
87

1✔
88
        if utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) {
2✔
89
                return r.deleteInstance(ctx, serviceInstance)
1✔
90
        }
1✔
91
        if len(serviceInstance.GetConditions()) == 0 {
2✔
92
                err := utils.InitConditions(ctx, r.Client, serviceInstance)
1✔
93
                if err != nil {
1✔
94
                        return ctrl.Result{}, err
×
95
                }
×
96
        }
97

98
        if isFinalState(ctx, serviceInstance) {
2✔
99
                if len(serviceInstance.Status.HashedSpec) == 0 {
2✔
100
                        updateHashedSpecValue(serviceInstance)
1✔
101
                        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
102
                }
1✔
103

104
                return ctrl.Result{}, nil
1✔
105
        }
106

107
        if len(serviceInstance.Status.OperationURL) > 0 {
2✔
108
                // ongoing operation - poll status from SM
1✔
109
                return r.poll(ctx, serviceInstance)
1✔
110
        }
1✔
111

112
        if controllerutil.AddFinalizer(serviceInstance, common.FinalizerName) {
2✔
113
                log.Info(fmt.Sprintf("added finalizer '%s' to service instance", common.FinalizerName))
1✔
114
                if err := r.Client.Update(ctx, serviceInstance); err != nil {
1✔
115
                        return ctrl.Result{}, err
×
116
                }
×
117
        }
118

119
        smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
120
        if err != nil {
1✔
121
                log.Error(err, "failed to get sm client")
×
122
                return utils.MarkAsTransientError(ctx, r.Client, common.Unknown, err, serviceInstance)
×
123
        }
×
124

125
        if serviceInstance.Status.InstanceID == "" {
2✔
126
                log.Info("Instance ID is empty, checking if instance exist in SM")
1✔
127
                smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
128
                if err != nil {
1✔
129
                        log.Error(err, "failed to check instance recovery")
×
130
                        return utils.MarkAsTransientError(ctx, r.Client, common.Unknown, err, serviceInstance)
×
131
                }
×
132
                if smInstance != nil {
2✔
133
                        return r.recover(ctx, smClient, serviceInstance, smInstance)
1✔
134
                }
1✔
135

136
                // if instance was not recovered then create new instance
137
                return r.createInstance(ctx, smClient, serviceInstance)
1✔
138
        }
139

140
        // Update
141
        if updateRequired(serviceInstance) {
2✔
142
                return r.updateInstance(ctx, smClient, serviceInstance)
1✔
143
        }
1✔
144

145
        // share/unshare
146
        if shareOrUnshareRequired(serviceInstance) {
2✔
147
                return r.handleInstanceSharing(ctx, serviceInstance, smClient)
1✔
148
        }
1✔
149

150
        log.Info("No action required")
1✔
151
        return ctrl.Result{}, nil
1✔
152
}
153

154
func (r *ServiceInstanceReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
155
        return ctrl.NewControllerManagedBy(mgr).
1✔
156
                For(&v1.ServiceInstance{}).
1✔
157
                WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](r.Config.RetryBaseDelay, r.Config.RetryMaxDelay)}).
1✔
158
                Complete(r)
1✔
159
}
1✔
160

161
func (r *ServiceInstanceReconciler) createInstance(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
162
        log := utils.GetLogger(ctx)
1✔
163
        log.Info("Creating instance in SM")
1✔
164
        updateHashedSpecValue(serviceInstance)
1✔
165
        instanceParameters, err := r.buildSMRequestParameters(ctx, serviceInstance)
1✔
166
        if err != nil {
2✔
167
                // if parameters are invalid there is nothing we can do, the user should fix it according to the error message in the condition
1✔
168
                log.Error(err, "failed to parse instance parameters")
1✔
169
                return utils.MarkAsTransientError(ctx, r.Client, smClientTypes.CREATE, err, serviceInstance)
1✔
170
        }
1✔
171

172
        provision, provisionErr := smClient.Provision(&smClientTypes.ServiceInstance{
1✔
173
                Name:          serviceInstance.Spec.ExternalName,
1✔
174
                ServicePlanID: serviceInstance.Spec.ServicePlanID,
1✔
175
                Parameters:    instanceParameters,
1✔
176
                Labels: smClientTypes.Labels{
1✔
177
                        common.NamespaceLabel: []string{serviceInstance.Namespace},
1✔
178
                        common.K8sNameLabel:   []string{serviceInstance.Name},
1✔
179
                        common.ClusterIDLabel: []string{r.Config.ClusterID},
1✔
180
                },
1✔
181
        }, serviceInstance.Spec.ServiceOfferingName, serviceInstance.Spec.ServicePlanName, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo), serviceInstance.Spec.DataCenter)
1✔
182

1✔
183
        if provisionErr != nil {
2✔
184
                var transientErr *sm.TransientError
1✔
185
                if errors.As(provisionErr, &transientErr) {
1✔
NEW
186
                        return utils.MarkAsTransientError(ctx, r.Client, smClientTypes.CREATE, provisionErr, serviceInstance)
×
NEW
187
                }
×
188
                log.Error(provisionErr, "failed to create service instance", "serviceOfferingName", serviceInstance.Spec.ServiceOfferingName,
1✔
189
                        "servicePlanName", serviceInstance.Spec.ServicePlanName)
1✔
190
                return utils.HandleError(ctx, r.Client, smClientTypes.CREATE, provisionErr, serviceInstance)
1✔
191
        }
192

193
        serviceInstance.Status.InstanceID = provision.InstanceID
1✔
194
        serviceInstance.Status.SubaccountID = provision.SubaccountID
1✔
195
        if len(provision.Tags) > 0 {
2✔
196
                tags, err := getTags(provision.Tags)
1✔
197
                if err != nil {
1✔
198
                        log.Error(err, "failed to unmarshal tags")
×
199
                } else {
1✔
200
                        serviceInstance.Status.Tags = tags
1✔
201
                }
1✔
202
        }
203

204
        if provision.Location != "" {
2✔
205
                log.Info("Provision request is in progress (async)")
1✔
206
                serviceInstance.Status.OperationURL = provision.Location
1✔
207
                serviceInstance.Status.OperationType = smClientTypes.CREATE
1✔
208
                utils.SetInProgressConditions(ctx, smClientTypes.CREATE, "", serviceInstance, false)
1✔
209

1✔
210
                return ctrl.Result{Requeue: true, RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
211
        }
1✔
212

213
        log.Info(fmt.Sprintf("Instance provisioned successfully, instanceID: %s, subaccountID: %s", serviceInstance.Status.InstanceID,
1✔
214
                serviceInstance.Status.SubaccountID))
1✔
215
        utils.SetSuccessConditions(smClientTypes.CREATE, serviceInstance, false)
1✔
216
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
217
}
218

219
func (r *ServiceInstanceReconciler) updateInstance(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
220
        log := utils.GetLogger(ctx)
1✔
221
        log.Info(fmt.Sprintf("updating instance %s in SM", serviceInstance.Status.InstanceID))
1✔
222

1✔
223
        instanceParameters, err := r.buildSMRequestParameters(ctx, serviceInstance)
1✔
224
        if err != nil {
2✔
225
                log.Error(err, "failed to parse instance parameters")
1✔
226
                return utils.MarkAsTransientError(ctx, r.Client, smClientTypes.UPDATE, err, serviceInstance)
1✔
227
        }
1✔
228

229
        updateHashedSpecValue(serviceInstance)
1✔
230
        _, operationURL, err := smClient.UpdateInstance(serviceInstance.Status.InstanceID, &smClientTypes.ServiceInstance{
1✔
231
                Name:          serviceInstance.Spec.ExternalName,
1✔
232
                ServicePlanID: serviceInstance.Spec.ServicePlanID,
1✔
233
                Parameters:    instanceParameters,
1✔
234
        }, serviceInstance.Spec.ServiceOfferingName, serviceInstance.Spec.ServicePlanName, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo), serviceInstance.Spec.DataCenter)
1✔
235

1✔
236
        if err != nil {
2✔
237
                log.Error(err, fmt.Sprintf("failed to update service instance with ID %s", serviceInstance.Status.InstanceID))
1✔
238
                return utils.HandleError(ctx, r.Client, smClientTypes.UPDATE, err, serviceInstance)
1✔
239
        }
1✔
240

241
        if operationURL != "" {
2✔
242
                log.Info(fmt.Sprintf("Update request accepted, operation URL: %s", operationURL))
1✔
243
                serviceInstance.Status.OperationURL = operationURL
1✔
244
                serviceInstance.Status.OperationType = smClientTypes.UPDATE
1✔
245
                utils.SetInProgressConditions(ctx, smClientTypes.UPDATE, "", serviceInstance, false)
1✔
246
                serviceInstance.Status.ForceReconcile = false
1✔
247
                if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
2✔
248
                        return ctrl.Result{}, err
1✔
249
                }
1✔
250

251
                return ctrl.Result{Requeue: true, RequeueAfter: r.Config.PollInterval}, nil
1✔
252
        }
253
        log.Info("Instance updated successfully")
1✔
254
        utils.SetSuccessConditions(smClientTypes.UPDATE, serviceInstance, false)
1✔
255
        serviceInstance.Status.ForceReconcile = false
1✔
256
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
257
}
258

259
func (r *ServiceInstanceReconciler) deleteInstance(ctx context.Context, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
260
        log := utils.GetLogger(ctx)
1✔
261

1✔
262
        log.Info("deleting instance")
1✔
263
        if controllerutil.ContainsFinalizer(serviceInstance, common.FinalizerName) {
2✔
264
                for key, secretName := range serviceInstance.Labels {
2✔
265
                        if strings.HasPrefix(key, common.InstanceSecretRefLabel) {
2✔
266
                                if err := utils.RemoveWatchForSecret(ctx, r.Client, types.NamespacedName{Name: secretName, Namespace: serviceInstance.Namespace}, string(serviceInstance.UID)); err != nil {
1✔
267
                                        log.Error(err, fmt.Sprintf("failed to unwatch secret %s", secretName))
×
268
                                        return ctrl.Result{}, err
×
269
                                }
×
270
                        }
271
                }
272

273
                smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
274
                if err != nil {
1✔
275
                        log.Error(err, "failed to get sm client")
×
276
                        return utils.MarkAsTransientError(ctx, r.Client, common.Unknown, err, serviceInstance)
×
277
                }
×
278
                if len(serviceInstance.Status.InstanceID) == 0 {
2✔
279
                        log.Info("No instance id found validating instance does not exists in SM before removing finalizer")
1✔
280
                        smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
281
                        if err != nil {
1✔
282
                                return ctrl.Result{}, err
×
283
                        }
×
284
                        if smInstance != nil {
2✔
285
                                log.Info("instance exists in SM continue with deletion")
1✔
286
                                serviceInstance.Status.InstanceID = smInstance.ID
1✔
287
                                utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "delete after recovery", serviceInstance, false)
1✔
288
                                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
289
                        }
1✔
290
                        log.Info("instance does not exists in SM, removing finalizer")
1✔
291
                        return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName)
1✔
292
                }
293

294
                if len(serviceInstance.Status.OperationURL) > 0 && serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
295
                        // ongoing delete operation - poll status from SM
1✔
296
                        return r.poll(ctx, serviceInstance)
1✔
297
                }
1✔
298

299
                log.Info(fmt.Sprintf("Deleting instance with id %v from SM", serviceInstance.Status.InstanceID))
1✔
300
                operationURL, deprovisionErr := smClient.Deprovision(serviceInstance.Status.InstanceID, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
301
                if deprovisionErr != nil {
2✔
302
                        // delete will proceed anyway
1✔
303
                        return utils.HandleDeleteError(ctx, r.Client, deprovisionErr, serviceInstance)
1✔
304
                }
1✔
305

306
                if operationURL != "" {
2✔
307
                        log.Info("Deleting instance async")
1✔
308
                        return r.handleAsyncDelete(ctx, serviceInstance, operationURL)
1✔
309
                }
1✔
310

311
                log.Info("Instance was deleted successfully, removing finalizer")
1✔
312
                // remove our finalizer from the list and update it.
1✔
313
                return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName)
1✔
314
        }
315
        return ctrl.Result{}, nil
1✔
316
}
317

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

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

349
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
350
}
351

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

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

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

426
        serviceInstance.Status.OperationURL = ""
1✔
427
        serviceInstance.Status.OperationType = ""
1✔
428

1✔
429
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
430
}
431

432
func (r *ServiceInstanceReconciler) handleAsyncDelete(ctx context.Context, serviceInstance *v1.ServiceInstance, opURL string) (ctrl.Result, error) {
1✔
433
        serviceInstance.Status.OperationURL = opURL
1✔
434
        serviceInstance.Status.OperationType = smClientTypes.DELETE
1✔
435
        utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "", serviceInstance, false)
1✔
436

1✔
437
        if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
1✔
438
                return ctrl.Result{}, err
×
439
        }
×
440

441
        return ctrl.Result{Requeue: true, RequeueAfter: r.Config.PollInterval}, nil
1✔
442
}
443

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

1✔
456
        instances, err := smClient.ListInstances(&parameters)
1✔
457
        if err != nil {
1✔
458
                log.Error(err, "failed to list instances in SM")
×
459
                return nil, err
×
460
        }
×
461

462
        if instances != nil && len(instances.ServiceInstances) > 0 {
2✔
463
                return &instances.ServiceInstances[0], nil
1✔
464
        }
1✔
465
        log.Info("instance not found in SM")
1✔
466
        return nil, nil
1✔
467
}
468

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

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

491
        instanceState := smClientTypes.SUCCEEDED
1✔
492
        operationType := smClientTypes.CREATE
1✔
493
        description := ""
1✔
494
        if smInstance.LastOperation != nil {
2✔
495
                instanceState = smInstance.LastOperation.State
1✔
496
                operationType = smInstance.LastOperation.Type
1✔
497
                description = smInstance.LastOperation.Description
1✔
498
        } else if !smInstance.Ready {
3✔
499
                instanceState = smClientTypes.FAILED
1✔
500
        }
1✔
501

502
        switch instanceState {
1✔
503
        case smClientTypes.PENDING:
1✔
504
                fallthrough
1✔
505
        case smClientTypes.INPROGRESS:
1✔
506
                k8sInstance.Status.OperationURL = sm.BuildOperationURL(smInstance.LastOperation.ID, smInstance.ID, smClientTypes.ServiceInstancesURL)
1✔
507
                k8sInstance.Status.OperationType = smInstance.LastOperation.Type
1✔
508
                utils.SetInProgressConditions(ctx, smInstance.LastOperation.Type, smInstance.LastOperation.Description, k8sInstance, false)
1✔
509
        case smClientTypes.SUCCEEDED:
1✔
510
                utils.SetSuccessConditions(operationType, k8sInstance, false)
1✔
511
        case smClientTypes.FAILED:
1✔
512
                utils.SetFailureConditions(operationType, description, k8sInstance, false)
1✔
513
        }
514

515
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, k8sInstance)
1✔
516
}
517

518
func (r *ServiceInstanceReconciler) handleInstanceSharingError(ctx context.Context, object common.SAPBTPResource, status metav1.ConditionStatus, reason string, err error) (ctrl.Result, error) {
1✔
519
        log := utils.GetLogger(ctx)
1✔
520

1✔
521
        errMsg := err.Error()
1✔
522
        isTransient := false
1✔
523

1✔
524
        if smError, ok := err.(*sm.ServiceManagerError); ok {
2✔
525
                log.Info(fmt.Sprintf("SM returned error with status code %d", smError.StatusCode))
1✔
526
                isTransient = utils.IsTransientError(smError, log)
1✔
527
                errMsg = smError.Error()
1✔
528

1✔
529
                if smError.StatusCode == http.StatusTooManyRequests {
2✔
530
                        errMsg = "in progress"
1✔
531
                        reason = common.InProgress
1✔
532
                } else if reason == common.ShareFailed &&
2✔
533
                        (smError.StatusCode == http.StatusBadRequest || smError.StatusCode == http.StatusInternalServerError) {
2✔
534
                        /* non-transient error may occur only when sharing
1✔
535
                           SM return 400 when plan is not sharable
1✔
536
                           SM returns 500 when TOGGLES_ENABLE_INSTANCE_SHARE_FROM_OPERATOR feature toggle is off */
1✔
537
                        reason = common.ShareNotSupported
1✔
538
                }
1✔
539
        }
540

541
        setSharedCondition(object, status, reason, errMsg)
1✔
542
        return ctrl.Result{Requeue: isTransient}, utils.UpdateStatus(ctx, r.Client, object)
1✔
543
}
544

545
func (r *ServiceInstanceReconciler) buildSMRequestParameters(ctx context.Context, serviceInstance *v1.ServiceInstance) ([]byte, error) {
1✔
546
        log := utils.GetLogger(ctx)
1✔
547
        instanceParameters, paramSecrets, err := utils.BuildSMRequestParameters(serviceInstance.Namespace, serviceInstance.Spec.Parameters, serviceInstance.Spec.ParametersFrom)
1✔
548
        if err != nil {
2✔
549
                log.Error(err, "failed to build instance parameters")
1✔
550
                return nil, err
1✔
551
        }
1✔
552
        instanceLabelsChanged := false
1✔
553
        newInstanceLabels := make(map[string]string)
1✔
554
        if serviceInstance.IsSubscribedToParamSecretsChanges() {
2✔
555
                // find all new secrets on the instance
1✔
556
                for _, secret := range paramSecrets {
2✔
557
                        labelKey := utils.GetLabelKeyForInstanceSecret(secret.Name)
1✔
558
                        newInstanceLabels[labelKey] = secret.Name
1✔
559
                        if _, ok := serviceInstance.Labels[labelKey]; !ok {
2✔
560
                                instanceLabelsChanged = true
1✔
561
                        }
1✔
562

563
                        if err := utils.AddWatchForSecretIfNeeded(ctx, r.Client, secret, string(serviceInstance.UID)); err != nil {
1✔
564
                                log.Error(err, fmt.Sprintf("failed to mark secret for watch %s", secret.Name))
×
565
                                return nil, err
×
566
                        }
×
567

568
                }
569
        }
570

571
        //sync instance labels
572
        for labelKey, labelValue := range serviceInstance.Labels {
2✔
573
                if strings.HasPrefix(labelKey, common.InstanceSecretRefLabel) {
2✔
574
                        if _, ok := newInstanceLabels[labelKey]; !ok {
2✔
575
                                log.Info(fmt.Sprintf("params secret named %s was removed, unwatching it", labelValue))
1✔
576
                                instanceLabelsChanged = true
1✔
577
                                if err := utils.RemoveWatchForSecret(ctx, r.Client, types.NamespacedName{Name: labelValue, Namespace: serviceInstance.Namespace}, string(serviceInstance.UID)); err != nil {
1✔
578
                                        log.Error(err, fmt.Sprintf("failed to unwatch secret %s", labelValue))
×
579
                                        return nil, err
×
580
                                }
×
581
                        }
582
                } else {
×
583
                        // this label not related to secrets, add it
×
584
                        newInstanceLabels[labelKey] = labelValue
×
585
                }
×
586
        }
587
        if instanceLabelsChanged {
2✔
588
                serviceInstance.Labels = newInstanceLabels
1✔
589
                log.Info("updating instance with secret labels")
1✔
590
                return instanceParameters, r.Client.Update(ctx, serviceInstance)
1✔
591
        }
1✔
592

593
        return instanceParameters, nil
1✔
594
}
595

596
func isFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) bool {
1✔
597
        log := utils.GetLogger(ctx)
1✔
598

1✔
599
        if serviceInstance.Status.ForceReconcile {
2✔
600
                log.Info("instance is not in final state, ForceReconcile is true")
1✔
601
                return false
1✔
602
        }
1✔
603

604
        if len(serviceInstance.Status.OperationURL) > 0 {
2✔
605
                log.Info(fmt.Sprintf("instance is not in final state, async operation is in progress (%s)", serviceInstance.Status.OperationURL))
1✔
606
                return false
1✔
607
        }
1✔
608

609
        observedGen := common.GetObservedGeneration(serviceInstance)
1✔
610
        if serviceInstance.Generation != observedGen {
2✔
611
                log.Info(fmt.Sprintf("instance is not in final state, generation: %d, observedGen: %d", serviceInstance.Generation, observedGen))
1✔
612
                return false
1✔
613
        }
1✔
614

615
        // succeeded=false for current generation, and without failed=true --> transient error retry
616
        if utils.IsInProgress(serviceInstance) {
2✔
617
                log.Info("instance is not in final state, sync operation is in progress")
1✔
618
                return false
1✔
619
        }
1✔
620

621
        if shareOrUnshareRequired(serviceInstance) {
2✔
622
                log.Info("instance is not in final state, need to sync sharing status")
1✔
623
                if len(serviceInstance.Status.HashedSpec) == 0 {
1✔
624
                        updateHashedSpecValue(serviceInstance)
×
625
                }
×
626
                return false
1✔
627
        }
628

629
        log.Info(fmt.Sprintf("instance is in final state (generation: %d)", serviceInstance.Generation))
1✔
630
        return true
1✔
631
}
632

633
func updateRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
634
        //update is not supported for failed instances (this can occur when instance creation was asynchronously)
1✔
635
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
1✔
636
                return false
×
637
        }
×
638

639
        if serviceInstance.Status.ForceReconcile {
2✔
640
                return true
1✔
641
        }
1✔
642

643
        cond := meta.FindStatusCondition(serviceInstance.Status.Conditions, common.ConditionSucceeded)
1✔
644
        if cond != nil && cond.Reason == common.UpdateInProgress { //in case of transient error occurred
2✔
645
                return true
1✔
646
        }
1✔
647

648
        return serviceInstance.GetSpecHash() != serviceInstance.Status.HashedSpec
1✔
649
}
650

651
func shareOrUnshareRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
652
        //relevant only for non-shared instances - sharing instance is possible only for usable instances
1✔
653
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
2✔
654
                return false
1✔
655
        }
1✔
656

657
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), common.ConditionShared)
1✔
658
        if sharedCondition == nil {
2✔
659
                return serviceInstance.GetShared()
1✔
660
        }
1✔
661

662
        if sharedCondition.Reason == common.ShareNotSupported {
2✔
663
                return false
1✔
664
        }
1✔
665

666
        if sharedCondition.Status == metav1.ConditionFalse {
2✔
667
                // instance does not appear to be shared, should share it if shared is requested
1✔
668
                return serviceInstance.GetShared()
1✔
669
        }
1✔
670

671
        // instance appears to be shared, should unshare it if shared is not requested
672
        return !serviceInstance.GetShared()
1✔
673
}
674

675
func getOfferingTags(smClient sm.Client, planID string) ([]string, error) {
1✔
676
        planQuery := &sm.Parameters{
1✔
677
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", planID)},
1✔
678
        }
1✔
679
        plans, err := smClient.ListPlans(planQuery)
1✔
680
        if err != nil {
1✔
681
                return nil, err
×
682
        }
×
683

684
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
685
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
686
        }
1✔
687

688
        offeringQuery := &sm.Parameters{
×
689
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
690
        }
×
691

×
692
        offerings, err := smClient.ListOfferings(offeringQuery)
×
693
        if err != nil {
×
694
                return nil, err
×
695
        }
×
696
        if offerings == nil || len(offerings.ServiceOfferings) != 1 {
×
697
                return nil, fmt.Errorf("could not find offering with id %s", plans.ServicePlans[0].ServiceOfferingID)
×
698
        }
×
699

700
        var tags []string
×
701
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
702
                return nil, err
×
703
        }
×
704
        return tags, nil
×
705
}
706

707
func getTags(tags []byte) ([]string, error) {
1✔
708
        var tagsArr []string
1✔
709
        if err := json.Unmarshal(tags, &tagsArr); err != nil {
1✔
710
                return nil, err
×
711
        }
×
712
        return tagsArr, nil
1✔
713
}
714

715
func setSharedCondition(object common.SAPBTPResource, status metav1.ConditionStatus, reason, msg string) {
1✔
716
        conditions := object.GetConditions()
1✔
717
        // align all conditions to latest generation
1✔
718
        for _, cond := range object.GetConditions() {
2✔
719
                if cond.Type != common.ConditionShared {
2✔
720
                        cond.ObservedGeneration = object.GetGeneration()
1✔
721
                        meta.SetStatusCondition(&conditions, cond)
1✔
722
                }
1✔
723
        }
724

725
        shareCondition := metav1.Condition{
1✔
726
                Type:    common.ConditionShared,
1✔
727
                Status:  status,
1✔
728
                Reason:  reason,
1✔
729
                Message: msg,
1✔
730
                // shared condition does not contain observed generation
1✔
731
        }
1✔
732

1✔
733
        // remove shared condition and add it as new (in case it has observed generation)
1✔
734
        meta.RemoveStatusCondition(&conditions, common.ConditionShared)
1✔
735
        meta.SetStatusCondition(&conditions, shareCondition)
1✔
736

1✔
737
        object.SetConditions(conditions)
1✔
738
}
739

740
func updateHashedSpecValue(serviceInstance *v1.ServiceInstance) {
1✔
741
        serviceInstance.Status.HashedSpec = serviceInstance.GetSpecHash()
1✔
742
}
1✔
743

744
func getErrorMsgFromLastOperation(status *smClientTypes.Operation) string {
1✔
745
        errMsg := "async operation error"
1✔
746
        if status == nil || len(status.Errors) == 0 {
1✔
747
                return errMsg
×
748
        }
×
749
        var errMap map[string]interface{}
1✔
750

1✔
751
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
752
                return errMsg
×
753
        }
×
754

755
        if description, found := errMap["description"]; found {
2✔
756
                if descStr, ok := description.(string); ok {
2✔
757
                        errMsg = descStr
1✔
758
                }
1✔
759
        }
760
        return errMsg
1✔
761
}
762

763
type SecretPredicate struct {
764
        predicate.Funcs
765
}
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