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

SAP / sap-btp-service-operator / 20505416479

25 Dec 2025 12:56PM UTC coverage: 78.22% (-0.2%) from 78.422%
20505416479

Pull #592

github

kerenlahav
cosmetics
Pull Request #592: [JIRA: SAPBTPCFS-28335] fips and update libs

6 of 11 new or added lines in 2 files covered. (54.55%)

4 existing lines in 2 files now uncovered.

2733 of 3494 relevant lines covered (78.22%)

0.88 hits per line

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

79.65
/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
        "strings"
24

25
        "github.com/SAP/sap-btp-service-operator/internal/utils/logutils"
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, logutils.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 len(serviceInstance.Status.OperationURL) > 0 {
2✔
99
                // ongoing operation - poll status from SM
1✔
100
                return r.poll(ctx, serviceInstance)
1✔
101
        }
1✔
102

103
        if isFinalState(ctx, serviceInstance) {
2✔
104
                if len(serviceInstance.Status.HashedSpec) == 0 {
2✔
105
                        updateHashedSpecValue(serviceInstance)
1✔
106
                        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
107
                }
1✔
108

109
                return ctrl.Result{}, nil
1✔
110
        }
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.HandleOperationFailure(ctx, r.Client, serviceInstance, common.Unknown, err)
×
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.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.CREATE, err)
×
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
        // If stored hash is MD5 (32 chars) and we're now using SHA256 (64 chars),
141
        // perform one-time migration by updating the stored hash without triggering update
142
        if len(serviceInstance.Status.HashedSpec) == 32 {
1✔
NEW
143
                // This is likely an MD5->SHA256 migration, update the stored hash silently
×
NEW
144
                // to prevent unnecessary service updates during FIPS migration
×
NEW
145
                updateHashedSpecValue(serviceInstance)
×
NEW
146
                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
×
NEW
147
        }
×
148

149
        if shouldUpdate := updateRequired(serviceInstance); shouldUpdate {
2✔
150
                return r.updateInstance(ctx, smClient, serviceInstance)
1✔
151
        }
1✔
152

153
        // share/unshare
154
        if shareOrUnshareRequired(serviceInstance) {
2✔
155
                return r.handleInstanceSharing(ctx, serviceInstance, smClient)
1✔
156
        }
1✔
157

158
        log.Info("No action required")
1✔
159
        return ctrl.Result{}, nil
1✔
160
}
161

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

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

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

1✔
191
        if provisionErr != nil {
2✔
192
                log.Error(provisionErr, "failed to create service instance", "serviceOfferingName", serviceInstance.Spec.ServiceOfferingName,
1✔
193
                        "servicePlanName", serviceInstance.Spec.ServicePlanName)
1✔
194
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.CREATE, provisionErr)
1✔
195
        }
1✔
196

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

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

1✔
214
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
215
        }
1✔
216

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

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

1✔
227
        instanceParameters, err := r.buildSMRequestParameters(ctx, serviceInstance)
1✔
228
        if err != nil {
2✔
229
                log.Error(err, "failed to parse instance parameters")
1✔
230
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, smClientTypes.UPDATE, err)
1✔
231
        }
1✔
232

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

1✔
240
        if err != nil {
2✔
241
                log.Error(err, fmt.Sprintf("failed to update service instance with ID %s", serviceInstance.Status.InstanceID))
1✔
242
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.UPDATE, err)
1✔
243
        }
1✔
244

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

255
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
256
        }
257
        log.Info("Instance updated successfully")
1✔
258
        utils.SetSuccessConditions(smClientTypes.UPDATE, serviceInstance, false)
1✔
259
        serviceInstance.Status.ForceReconcile = false
1✔
260
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
261
}
262

263
func (r *ServiceInstanceReconciler) deleteInstance(ctx context.Context, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
264
        log := logutils.GetLogger(ctx)
1✔
265

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

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

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

303
                log.Info(fmt.Sprintf("Deleting instance with id %v from SM", serviceInstance.Status.InstanceID))
1✔
304
                operationURL, deprovisionErr := smClient.Deprovision(serviceInstance.Status.InstanceID, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
305
                if deprovisionErr != nil {
2✔
306
                        return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.DELETE, deprovisionErr)
1✔
307
                }
1✔
308

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

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

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

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

352
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
353
}
354

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

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

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

429
        serviceInstance.Status.OperationURL = ""
1✔
430
        serviceInstance.Status.OperationType = ""
1✔
431

1✔
432
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
433
}
434

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

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

444
        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
445
}
446

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

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

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

472
func (r *ServiceInstanceReconciler) recover(ctx context.Context, smClient sm.Client, k8sInstance *v1.ServiceInstance, smInstance *smClientTypes.ServiceInstance) (ctrl.Result, error) {
1✔
473
        log := logutils.GetLogger(ctx)
1✔
474

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

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

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

518
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, k8sInstance)
1✔
519
}
520

521
func (r *ServiceInstanceReconciler) buildSMRequestParameters(ctx context.Context, serviceInstance *v1.ServiceInstance) ([]byte, error) {
1✔
522
        log := logutils.GetLogger(ctx)
1✔
523
        instanceParameters, paramSecrets, err := utils.BuildSMRequestParameters(serviceInstance.Namespace, serviceInstance.Spec.Parameters, serviceInstance.Spec.ParametersFrom)
1✔
524
        if err != nil {
2✔
525
                log.Error(err, "failed to build instance parameters")
1✔
526
                return nil, err
1✔
527
        }
1✔
528
        instanceLabelsChanged := false
1✔
529
        newInstanceLabels := make(map[string]string)
1✔
530
        if serviceInstance.IsSubscribedToParamSecretsChanges() {
2✔
531
                // find all new secrets on the instance
1✔
532
                for _, secret := range paramSecrets {
2✔
533
                        labelKey := utils.GetLabelKeyForInstanceSecret(secret.Name)
1✔
534
                        newInstanceLabels[labelKey] = secret.Name
1✔
535
                        if _, ok := serviceInstance.Labels[labelKey]; !ok {
2✔
536
                                instanceLabelsChanged = true
1✔
537
                        }
1✔
538

539
                        if err := utils.AddWatchForSecretIfNeeded(ctx, r.Client, secret, string(serviceInstance.UID)); err != nil {
1✔
540
                                log.Error(err, fmt.Sprintf("failed to mark secret for watch %s", secret.Name))
×
541
                                return nil, err
×
542
                        }
×
543

544
                }
545
        }
546

547
        //sync instance labels
548
        for labelKey, labelValue := range serviceInstance.Labels {
2✔
549
                if strings.HasPrefix(labelKey, common.InstanceSecretRefLabel) {
2✔
550
                        if _, ok := newInstanceLabels[labelKey]; !ok {
2✔
551
                                log.Info(fmt.Sprintf("params secret named %s was removed, unwatching it", labelValue))
1✔
552
                                instanceLabelsChanged = true
1✔
553
                                if err := utils.RemoveWatchForSecret(ctx, r.Client, types.NamespacedName{Name: labelValue, Namespace: serviceInstance.Namespace}, string(serviceInstance.UID)); err != nil {
1✔
554
                                        log.Error(err, fmt.Sprintf("failed to unwatch secret %s", labelValue))
×
555
                                        return nil, err
×
556
                                }
×
557
                        }
558
                } else {
×
559
                        // this label not related to secrets, add it
×
560
                        newInstanceLabels[labelKey] = labelValue
×
561
                }
×
562
        }
563
        if instanceLabelsChanged {
2✔
564
                serviceInstance.Labels = newInstanceLabels
1✔
565
                log.Info("updating instance with secret labels")
1✔
566
                return instanceParameters, r.Client.Update(ctx, serviceInstance)
1✔
567
        }
1✔
568

569
        return instanceParameters, nil
1✔
570
}
571

572
func isFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) bool {
1✔
573
        log := logutils.GetLogger(ctx)
1✔
574

1✔
575
        if serviceInstance.Status.ForceReconcile {
2✔
576
                log.Info("instance is not in final state, ForceReconcile is true")
1✔
577
                return false
1✔
578
        }
1✔
579

580
        observedGen := common.GetObservedGeneration(serviceInstance)
1✔
581
        if serviceInstance.Generation != observedGen {
2✔
582
                log.Info(fmt.Sprintf("instance is not in final state, generation: %d, observedGen: %d", serviceInstance.Generation, observedGen))
1✔
583
                return false
1✔
584
        }
1✔
585

586
        if utils.ShouldRetryOperation(serviceInstance) {
2✔
587
                log.Info("instance is not in final state, last operation failed, retrying")
1✔
588
                return false
1✔
589
        }
1✔
590

591
        if shareOrUnshareRequired(serviceInstance) {
2✔
592
                log.Info("instance is not in final state, need to sync sharing status")
1✔
593
                if len(serviceInstance.Status.HashedSpec) == 0 {
1✔
594
                        updateHashedSpecValue(serviceInstance)
×
595
                }
×
596
                return false
1✔
597
        }
598

599
        log.Info(fmt.Sprintf("instance is in final state (generation: %d)", serviceInstance.Generation))
1✔
600
        return true
1✔
601
}
602

603
func updateRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
604
        //update is not supported for failed instances (this can occur when instance creation was asynchronously)
1✔
605
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
1✔
606
                return false
×
607
        }
×
608

609
        if serviceInstance.Status.ForceReconcile {
2✔
610
                return true
1✔
611
        }
1✔
612

613
        cond := meta.FindStatusCondition(serviceInstance.Status.Conditions, common.ConditionSucceeded)
1✔
614
        if cond != nil && cond.Reason == common.UpdateInProgress { //in case of transient error occurred
1✔
615
                return true
×
616
        }
×
617

618
        currentHash := serviceInstance.GetSpecHash()
1✔
619
        storedHash := serviceInstance.Status.HashedSpec
1✔
620

1✔
621
        return currentHash != storedHash
1✔
622
}
623

624
func shareOrUnshareRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
625
        //relevant only for non-shared instances - sharing instance is possible only for usable instances
1✔
626
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
2✔
627
                return false
1✔
628
        }
1✔
629

630
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), common.ConditionShared)
1✔
631
        if sharedCondition == nil {
2✔
632
                return serviceInstance.GetShared()
1✔
633
        }
1✔
634

635
        if sharedCondition.Reason == common.ShareNotSupported {
2✔
636
                return false
1✔
637
        }
1✔
638

639
        if sharedCondition.Status == metav1.ConditionFalse {
2✔
640
                // instance does not appear to be shared, should share it if shared is requested
1✔
641
                return serviceInstance.GetShared()
1✔
642
        }
1✔
643

644
        // instance appears to be shared, should unshare it if shared is not requested
645
        return !serviceInstance.GetShared()
1✔
646
}
647

648
func getOfferingTags(smClient sm.Client, planID string) ([]string, error) {
1✔
649
        planQuery := &sm.Parameters{
1✔
650
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", planID)},
1✔
651
        }
1✔
652
        plans, err := smClient.ListPlans(planQuery)
1✔
653
        if err != nil {
1✔
654
                return nil, err
×
655
        }
×
656

657
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
658
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
659
        }
1✔
660

661
        offeringQuery := &sm.Parameters{
×
662
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
663
        }
×
664

×
665
        offerings, err := smClient.ListOfferings(offeringQuery)
×
666
        if err != nil {
×
667
                return nil, err
×
668
        }
×
669
        if offerings == nil || len(offerings.ServiceOfferings) != 1 {
×
670
                return nil, fmt.Errorf("could not find offering with id %s", plans.ServicePlans[0].ServiceOfferingID)
×
671
        }
×
672

673
        var tags []string
×
674
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
675
                return nil, err
×
676
        }
×
677
        return tags, nil
×
678
}
679

680
func getTags(tags []byte) ([]string, error) {
1✔
681
        var tagsArr []string
1✔
682
        if err := json.Unmarshal(tags, &tagsArr); err != nil {
1✔
683
                return nil, err
×
684
        }
×
685
        return tagsArr, nil
1✔
686
}
687

688
func updateHashedSpecValue(serviceInstance *v1.ServiceInstance) {
1✔
689
        serviceInstance.Status.HashedSpec = serviceInstance.GetSpecHash()
1✔
690
}
1✔
691

692
func getErrorMsgFromLastOperation(status *smClientTypes.Operation) string {
1✔
693
        errMsg := "async operation error"
1✔
694
        if status == nil || len(status.Errors) == 0 {
1✔
695
                return errMsg
×
696
        }
×
697
        var errMap map[string]interface{}
1✔
698

1✔
699
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
700
                return errMsg
×
701
        }
×
702

703
        if description, found := errMap["description"]; found {
2✔
704
                if descStr, ok := description.(string); ok {
2✔
705
                        errMsg = descStr
1✔
706
                }
1✔
707
        }
708
        return errMsg
1✔
709
}
710

711
type SecretPredicate struct {
712
        predicate.Funcs
713
}
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