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

SAP / sap-btp-service-operator / 20485787841

24 Dec 2025 12:04PM UTC coverage: 78.52% (+0.1%) from 78.422%
20485787841

Pull #592

github

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

20 of 23 new or added lines in 2 files covered. (86.96%)

1 existing line in 1 file now uncovered.

2749 of 3501 relevant lines covered (78.52%)

0.88 hits per line

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

81.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
        "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
        // Update
141
        shouldUpdate, shouldUpdateState := updateRequired(serviceInstance)
1✔
142
        if shouldUpdate {
2✔
143
                return r.updateInstance(ctx, smClient, serviceInstance)
1✔
144
        }
1✔
145
        if shouldUpdateState {
1✔
NEW
146
                log.Info(fmt.Sprintf("updated instance hashing '%s' to '%s'", serviceInstance.Name, serviceInstance.Status.InstanceID))
×
NEW
147
                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
×
NEW
148
        }
×
149

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

541
                }
542
        }
543

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

566
        return instanceParameters, nil
1✔
567
}
568

569
func isFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) bool {
1✔
570
        log := logutils.GetLogger(ctx)
1✔
571

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

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

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

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

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

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

606
        if serviceInstance.Status.ForceReconcile {
2✔
607
                return true, false
1✔
608
        }
1✔
609

610
        cond := meta.FindStatusCondition(serviceInstance.Status.Conditions, common.ConditionSucceeded)
1✔
611
        if cond != nil && cond.Reason == common.UpdateInProgress { //in case of transient error occurred
2✔
612
                return true, false
1✔
613
        }
1✔
614

615
        currentHash := serviceInstance.GetSpecHash()
1✔
616
        storedHash := serviceInstance.Status.HashedSpec
1✔
617

1✔
618
        // If stored hash is MD5 (32 chars) and we're now using SHA256 (64 chars),
1✔
619
        // perform one-time migration by updating the stored hash without triggering update
1✔
620
        if len(storedHash) == 32 && len(currentHash) == 64 {
2✔
621
                // This is likely an MD5->SHA256 migration, update the stored hash silently
1✔
622
                // to prevent unnecessary service updates during FIPS migration
1✔
623
                updateHashedSpecValue(serviceInstance)
1✔
624
                return false, true // No actual spec change, just hash algorithm migration
1✔
625
        }
1✔
626

627
        return currentHash != storedHash, false
1✔
628
}
629

630
func shareOrUnshareRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
631
        //relevant only for non-shared instances - sharing instance is possible only for usable instances
1✔
632
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
2✔
633
                return false
1✔
634
        }
1✔
635

636
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), common.ConditionShared)
1✔
637
        if sharedCondition == nil {
2✔
638
                return serviceInstance.GetShared()
1✔
639
        }
1✔
640

641
        if sharedCondition.Reason == common.ShareNotSupported {
2✔
642
                return false
1✔
643
        }
1✔
644

645
        if sharedCondition.Status == metav1.ConditionFalse {
2✔
646
                // instance does not appear to be shared, should share it if shared is requested
1✔
647
                return serviceInstance.GetShared()
1✔
648
        }
1✔
649

650
        // instance appears to be shared, should unshare it if shared is not requested
651
        return !serviceInstance.GetShared()
1✔
652
}
653

654
func getOfferingTags(smClient sm.Client, planID string) ([]string, error) {
1✔
655
        planQuery := &sm.Parameters{
1✔
656
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", planID)},
1✔
657
        }
1✔
658
        plans, err := smClient.ListPlans(planQuery)
1✔
659
        if err != nil {
1✔
660
                return nil, err
×
661
        }
×
662

663
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
664
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
665
        }
1✔
666

667
        offeringQuery := &sm.Parameters{
×
668
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
669
        }
×
670

×
671
        offerings, err := smClient.ListOfferings(offeringQuery)
×
672
        if err != nil {
×
673
                return nil, err
×
674
        }
×
675
        if offerings == nil || len(offerings.ServiceOfferings) != 1 {
×
676
                return nil, fmt.Errorf("could not find offering with id %s", plans.ServicePlans[0].ServiceOfferingID)
×
677
        }
×
678

679
        var tags []string
×
680
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
681
                return nil, err
×
682
        }
×
683
        return tags, nil
×
684
}
685

686
func getTags(tags []byte) ([]string, error) {
1✔
687
        var tagsArr []string
1✔
688
        if err := json.Unmarshal(tags, &tagsArr); err != nil {
1✔
689
                return nil, err
×
690
        }
×
691
        return tagsArr, nil
1✔
692
}
693

694
func updateHashedSpecValue(serviceInstance *v1.ServiceInstance) {
1✔
695
        serviceInstance.Status.HashedSpec = serviceInstance.GetSpecHash()
1✔
696
}
1✔
697

698
func getErrorMsgFromLastOperation(status *smClientTypes.Operation) string {
1✔
699
        errMsg := "async operation error"
1✔
700
        if status == nil || len(status.Errors) == 0 {
1✔
701
                return errMsg
×
702
        }
×
703
        var errMap map[string]interface{}
1✔
704

1✔
705
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
706
                return errMsg
×
707
        }
×
708

709
        if description, found := errMap["description"]; found {
2✔
710
                if descStr, ok := description.(string); ok {
2✔
711
                        errMsg = descStr
1✔
712
                }
1✔
713
        }
714
        return errMsg
1✔
715
}
716

717
type SecretPredicate struct {
718
        predicate.Funcs
719
}
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