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

SAP / sap-btp-service-operator / 12466536974

23 Dec 2024 12:07PM UTC coverage: 80.589% (+1.2%) from 79.425%
12466536974

Pull #486

github

I065450
.
Pull Request #486: subscribe to secret change

135 of 194 new or added lines in 9 files covered. (69.59%)

2 existing lines in 1 file now uncovered.

2819 of 3498 relevant lines covered (80.59%)

0.91 hits per line

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

82.42
/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 {
2✔
100
                                return ctrl.Result{}, err
1✔
101
                        }
1✔
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.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.NewItemExponentialFailureRateLimiter(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
                log.Error(provisionErr, "failed to create service instance", "serviceOfferingName", serviceInstance.Spec.ServiceOfferingName,
1✔
185
                        "servicePlanName", serviceInstance.Spec.ServicePlanName)
1✔
186
                return utils.HandleError(ctx, r.Client, smClientTypes.CREATE, provisionErr, serviceInstance)
1✔
187
        }
1✔
188

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

589
        return instanceParameters, nil
1✔
590
}
591

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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