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

SAP / sap-btp-service-operator / 16544714803

26 Jul 2025 10:59PM UTC coverage: 80.36% (+0.1%) from 80.217%
16544714803

Pull #543

github

web-flow
Create newdocu.md
Pull Request #543: NEW VERSION DOCU.md

2815 of 3503 relevant lines covered (80.36%)

0.9 hits per line

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

82.06
/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
                        err := r.Client.Status().Update(ctx, serviceInstance)
1✔
102
                        if err != nil {
1✔
103
                                return ctrl.Result{}, err
×
104
                        }
×
105
                }
106

107
                return ctrl.Result{}, nil
1✔
108
        }
109

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

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

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

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

139
                // if instance was not recovered then create new instance
140
                return r.createInstance(ctx, smClient, serviceInstance)
1✔
141
        }
142

143
        // Update
144
        if updateRequired(serviceInstance) {
2✔
145
                return r.updateInstance(ctx, smClient, serviceInstance)
1✔
146
        }
1✔
147

148
        // share/unshare
149
        if shareOrUnshareRequired(serviceInstance) {
2✔
150
                return r.handleInstanceSharing(ctx, serviceInstance, smClient)
1✔
151
        }
1✔
152

153
        log.Info("No action required")
1✔
154
        return ctrl.Result{}, nil
1✔
155
}
156

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

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

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

1✔
186
        if provisionErr != nil {
2✔
187
                log.Error(provisionErr, "failed to create service instance", "serviceOfferingName", serviceInstance.Spec.ServiceOfferingName,
1✔
188
                        "servicePlanName", serviceInstance.Spec.ServicePlanName)
1✔
189
                return utils.HandleError(ctx, r.Client, smClientTypes.CREATE, provisionErr, serviceInstance)
1✔
190
        }
1✔
191

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

567
                }
568
        }
569

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

592
        return instanceParameters, nil
1✔
593
}
594

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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