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

SAP / sap-btp-service-operator / 12391811373

18 Dec 2024 11:14AM UTC coverage: 80.062% (+0.9%) from 79.157%
12391811373

Pull #486

github

I065450
[SAPBTPCFS-15469] Update Service Instance on change of 'parametersFrom' secret
Pull Request #486: subscribe to secret change

135 of 187 new or added lines in 9 files covered. (72.19%)

4 existing lines in 1 file now uncovered.

2823 of 3526 relevant lines covered (80.06%)

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
        "k8s.io/apimachinery/pkg/types"
27

28
        "sigs.k8s.io/controller-runtime/pkg/predicate"
29

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

37
        "k8s.io/client-go/util/workqueue"
38
        "sigs.k8s.io/controller-runtime/pkg/controller"
39

40
        v1 "github.com/SAP/sap-btp-service-operator/api/v1"
41
        "k8s.io/apimachinery/pkg/api/meta"
42

43
        "github.com/google/uuid"
44

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

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

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

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

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

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

95
        if isFinalState(ctx, serviceInstance) {
2✔
96
                if len(serviceInstance.Status.HashedSpec) == 0 {
2✔
97
                        updateHashedSpecValue(serviceInstance)
1✔
98
                        err := r.Client.Status().Update(ctx, serviceInstance)
1✔
99
                        if err != nil {
1✔
100
                                return ctrl.Result{}, err
×
101
                        }
×
102
                }
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.ContainsFinalizer(serviceInstance, common.FinalizerName) {
2✔
113
                controllerutil.AddFinalizer(serviceInstance, common.FinalizerName)
1✔
114
                log.Info(fmt.Sprintf("added finalizer '%s' to service instance", common.FinalizerName))
1✔
115
                if err := r.Client.Update(ctx, serviceInstance); err != nil {
1✔
116
                        return ctrl.Result{}, err
×
117
                }
×
118
        }
119

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
259
        log.Info("deleting instance")
1✔
260
        if controllerutil.ContainsFinalizer(serviceInstance, common.FinalizerName) {
2✔
261
                if serviceInstance.Labels != nil {
2✔
262
                        for key, secretName := range serviceInstance.Labels {
2✔
263
                                if strings.HasPrefix(key, common.InstanceSecretRefLabel) {
2✔
264
                                        if err := utils.RemoveWatchForSecret(ctx, r.Client, types.NamespacedName{Name: secretName, Namespace: serviceInstance.Namespace}, string(serviceInstance.UID)); err != nil {
1✔
NEW
265
                                                log.Error(err, fmt.Sprintf("failed to unwatch secret %s", secretName))
×
NEW
266
                                        }
×
267
                                }
268
                        }
269
                }
270
                smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
271
                if err != nil {
1✔
272
                        log.Error(err, "failed to get sm client")
×
273
                        return utils.MarkAsTransientError(ctx, r.Client, common.Unknown, err, serviceInstance)
×
274
                }
×
275
                if len(serviceInstance.Status.InstanceID) == 0 {
2✔
276
                        log.Info("No instance id found validating instance does not exists in SM before removing finalizer")
1✔
277
                        smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
278
                        if err != nil {
1✔
279
                                return ctrl.Result{}, err
×
280
                        }
×
281
                        if smInstance != nil {
2✔
282
                                log.Info("instance exists in SM continue with deletion")
1✔
283
                                serviceInstance.Status.InstanceID = smInstance.ID
1✔
284
                                utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "delete after recovery", serviceInstance, false)
1✔
285
                                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
286
                        }
1✔
287
                        log.Info("instance does not exists in SM, removing finalizer")
1✔
288
                        return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName, serviceInstance.GetControllerName())
1✔
289
                }
290

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

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

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

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

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

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

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

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

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

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

423
        serviceInstance.Status.OperationURL = ""
1✔
424
        serviceInstance.Status.OperationType = ""
1✔
425

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
518
        errMsg := err.Error()
1✔
519
        isTransient := false
1✔
520

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

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

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

542
func (r *ServiceInstanceReconciler) buildSMRequestParameters(ctx context.Context, serviceInstance *v1.ServiceInstance) ([]byte, error) {
1✔
543
        log := utils.GetLogger(ctx)
1✔
544
        instanceParameters, paramSecrets, err := utils.BuildSMRequestParameters(serviceInstance.Namespace, serviceInstance.Spec.Parameters, serviceInstance.Spec.ParametersFrom)
1✔
545
        if err != nil {
2✔
546
                log.Error(err, "failed to build instance parameters")
1✔
547
                return nil, err
1✔
548
        }
1✔
549
        instanceLabelsChanged := false
1✔
550
        instanceLabels := make(map[string]string)
1✔
551
        if serviceInstance.IsSubscribedToSecretChange() {
2✔
552
                // find all new secrets on the instance
1✔
553
                for secretUID := range paramSecrets {
2✔
554
                        secret := paramSecrets[secretUID]
1✔
555
                        instanceLabels[common.InstanceSecretRefLabel+secretUID] = secret.Name
1✔
556
                        if _, ok := serviceInstance.Labels[common.InstanceSecretRefLabel+secretUID]; !ok {
2✔
557
                                log.Info(fmt.Sprintf("adding secret watch for secret %s", secret.Name))
1✔
558
                                instanceLabelsChanged = true
1✔
559
                                if err := utils.AddWatchForSecret(ctx, r.Client, secret, string(serviceInstance.UID)); err != nil {
1✔
NEW
560
                                        log.Error(err, fmt.Sprintf("failed to mark secret for watch %s", secret.Name))
×
NEW
561
                                        return nil, err
×
NEW
562
                                }
×
563
                        }
564
                }
565
        }
566

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

588
        return instanceParameters, nil
1✔
589
}
590

591
func isFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) bool {
1✔
592
        log := utils.GetLogger(ctx)
1✔
593

1✔
594
        if serviceInstance.Status.ForceReconcile {
2✔
595
                log.Info("instance is not in final state, ForceReconcile is true")
1✔
596
                return false
1✔
597
        }
1✔
598

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

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

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

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

624
        log.Info(fmt.Sprintf("instance is in final state (generation: %d)", serviceInstance.Generation))
1✔
625
        return true
1✔
626
}
627

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

634
        if serviceInstance.Status.ForceReconcile {
2✔
635
                return true
1✔
636
        }
1✔
637

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

643
        return serviceInstance.GetSpecHash() != serviceInstance.Status.HashedSpec
1✔
644
}
645

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

652
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), common.ConditionShared)
1✔
653
        if sharedCondition == nil {
2✔
654
                return serviceInstance.GetShared()
1✔
655
        }
1✔
656

657
        if sharedCondition.Reason == common.ShareNotSupported {
2✔
658
                return false
1✔
659
        }
1✔
660

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

666
        // instance appears to be shared, should unshare it if shared is not requested
667
        return !serviceInstance.GetShared()
1✔
668
}
669

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

679
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
680
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
681
        }
1✔
682

683
        offeringQuery := &sm.Parameters{
×
684
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
685
        }
×
686

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

695
        var tags []string
×
696
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
697
                return nil, err
×
698
        }
×
699
        return tags, nil
×
700
}
701

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

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

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

1✔
728
        // remove shared condition and add it as new (in case it has observed generation)
1✔
729
        meta.RemoveStatusCondition(&conditions, common.ConditionShared)
1✔
730
        meta.SetStatusCondition(&conditions, shareCondition)
1✔
731

1✔
732
        object.SetConditions(conditions)
1✔
733
}
734

735
func updateHashedSpecValue(serviceInstance *v1.ServiceInstance) {
1✔
736
        serviceInstance.Status.HashedSpec = serviceInstance.GetSpecHash()
1✔
737
}
1✔
738

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

1✔
746
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
747
                return errMsg
×
748
        }
×
749

750
        if description, found := errMap["description"]; found {
2✔
751
                if descStr, ok := description.(string); ok {
2✔
752
                        errMsg = descStr
1✔
753
                }
1✔
754
        }
755
        return errMsg
1✔
756
}
757

758
type SecretPredicate struct {
759
        predicate.Funcs
760
}
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