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

SAP / sap-btp-service-operator / 22060869981

16 Feb 2026 11:26AM UTC coverage: 78.123% (-0.3%) from 78.394%
22060869981

push

github

web-flow
BUG fix - OOM crash when cluster contains a lot of secrets  (#610)

44 of 63 new or added lines in 3 files covered. (69.84%)

13 existing lines in 3 files now uncovered.

2814 of 3602 relevant lines covered (78.12%)

0.88 hits per line

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

80.39
/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
        "time"
26

27
        "github.com/SAP/sap-btp-service-operator/internal/utils/logutils"
28
        "github.com/pkg/errors"
29
        corev1 "k8s.io/api/core/v1"
30
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
31

32
        "k8s.io/apimachinery/pkg/types"
33

34
        "sigs.k8s.io/controller-runtime/pkg/predicate"
35

36
        "github.com/SAP/sap-btp-service-operator/api/common"
37
        "github.com/SAP/sap-btp-service-operator/internal/config"
38
        "github.com/SAP/sap-btp-service-operator/internal/utils"
39
        "github.com/go-logr/logr"
40
        "k8s.io/apimachinery/pkg/runtime"
41
        "k8s.io/client-go/tools/record"
42

43
        "k8s.io/client-go/util/workqueue"
44
        "sigs.k8s.io/controller-runtime/pkg/controller"
45

46
        v1 "github.com/SAP/sap-btp-service-operator/api/v1"
47
        "k8s.io/apimachinery/pkg/api/meta"
48

49
        "github.com/google/uuid"
50

51
        "github.com/SAP/sap-btp-service-operator/client/sm"
52
        smClientTypes "github.com/SAP/sap-btp-service-operator/client/sm/types"
53
        apierrors "k8s.io/apimachinery/pkg/api/errors"
54
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
55
        ctrl "sigs.k8s.io/controller-runtime"
56
        "sigs.k8s.io/controller-runtime/pkg/client"
57
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
58
)
59

60
// ServiceInstanceReconciler reconciles a ServiceInstance object
61
type ServiceInstanceReconciler struct {
62
        client.Client
63
        Log         logr.Logger
64
        Scheme      *runtime.Scheme
65
        GetSMClient func(ctx context.Context, serviceInstance *v1.ServiceInstance) (sm.Client, error)
66
        Config      config.Config
67
        Recorder    record.EventRecorder
68
}
69

70
// +kubebuilder:rbac:groups=services.cloud.sap.com,resources=serviceinstances,verbs=get;list;watch;create;update;patch;delete
71
// +kubebuilder:rbac:groups=services.cloud.sap.com,resources=serviceinstances/status,verbs=get;update;patch
72
// +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch;delete
73
// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;create;update
74

75
func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
1✔
76
        log := r.Log.WithValues("serviceinstance", req.NamespacedName).WithValues("correlation_id", uuid.New().String())
1✔
77
        ctx = context.WithValue(ctx, logutils.LogKey, log)
1✔
78

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

1✔
91
        if shouldInstanceBeDeleted(serviceInstance) {
2✔
92
                return r.deleteInstance(ctx, serviceInstance)
1✔
93
        }
1✔
94

95
        // If stored hash is MD5 (32 chars) and we're now using SHA256 (64 chars),
96
        // perform one-time migration by updating the stored hash without triggering update
97
        if len(serviceInstance.Status.HashedSpec) == 32 {
2✔
98
                // This is likely an MD5->SHA256 migration, update the stored hash silently
1✔
99
                // to prevent unnecessary service updates during FIPS migration
1✔
100
                log.Info(fmt.Sprintf("updated hashing for instance '%s' (id=%s)", serviceInstance.Name, serviceInstance.Status.InstanceID))
1✔
101
                updateHashedSpecValue(serviceInstance)
1✔
102
                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
103
        }
1✔
104

105
        smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
106
        if err != nil {
1✔
107
                log.Error(err, "failed to get sm client")
×
108
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, common.Unknown, err)
×
109
        }
×
110
        if len(serviceInstance.Status.InstanceID) > 0 {
2✔
111
                if _, err := smClient.GetInstanceByID(serviceInstance.Status.InstanceID, nil); err != nil {
2✔
112
                        var smError *sm.ServiceManagerError
1✔
113
                        if ok := errors.As(err, &smError); ok {
2✔
114
                                if smError.StatusCode == http.StatusNotFound {
2✔
115
                                        log.Info(fmt.Sprintf("instance %s not found in SM", serviceInstance.Status.InstanceID))
1✔
116
                                        condition := metav1.Condition{
1✔
117
                                                Type:               common.ConditionReady,
1✔
118
                                                Status:             metav1.ConditionFalse,
1✔
119
                                                ObservedGeneration: serviceInstance.Generation,
1✔
120
                                                LastTransitionTime: metav1.NewTime(time.Now()),
1✔
121
                                                Reason:             common.ResourceNotFound,
1✔
122
                                                Message:            fmt.Sprintf(common.ResourceNotFoundMessageFormat, "instance", serviceInstance.Status.InstanceID),
1✔
123
                                        }
1✔
124
                                        serviceInstance.Status.Conditions = []metav1.Condition{condition}
1✔
125
                                        serviceInstance.Status.Ready = metav1.ConditionFalse
1✔
126
                                        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
127
                                }
1✔
128
                        }
129
                        log.Error(err, fmt.Sprintf("failed to get instance %s from SM", serviceInstance.Status.InstanceID))
×
130
                        return ctrl.Result{}, err
×
131
                }
132
        }
133

134
        if len(serviceInstance.GetConditions()) == 0 {
2✔
135
                err := utils.InitConditions(ctx, r.Client, serviceInstance)
1✔
136
                if err != nil {
1✔
137
                        return ctrl.Result{}, err
×
138
                }
×
139
        }
140

141
        if len(serviceInstance.Status.OperationURL) > 0 {
2✔
142
                // ongoing operation - poll status from SM
1✔
143
                return r.poll(ctx, serviceInstance)
1✔
144
        }
1✔
145

146
        if isFinalState(ctx, serviceInstance) {
2✔
147
                return r.maintainFinalState(ctx, serviceInstance)
1✔
148
        }
1✔
149

150
        if controllerutil.AddFinalizer(serviceInstance, common.FinalizerName) {
2✔
151
                log.Info(fmt.Sprintf("added finalizer '%s' to service instance", common.FinalizerName))
1✔
152
                if err := r.Client.Update(ctx, serviceInstance); err != nil {
1✔
153
                        return ctrl.Result{}, err
×
154
                }
×
155
        }
156

157
        if serviceInstance.Status.InstanceID == "" {
2✔
158
                log.Info("Instance ID is empty, checking if instance exist in SM")
1✔
159
                smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
160
                if err != nil {
1✔
161
                        log.Error(err, "failed to check instance recovery")
×
162
                        return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.CREATE, err)
×
163
                }
×
164
                if smInstance != nil {
2✔
165
                        return r.recover(ctx, smClient, serviceInstance, smInstance)
1✔
166
                }
1✔
167

168
                // if instance was not recovered then create new instance
169
                return r.createInstance(ctx, smClient, serviceInstance)
1✔
170
        }
171

172
        if updateRequired(serviceInstance) {
2✔
173
                return r.updateInstance(ctx, smClient, serviceInstance)
1✔
174
        }
1✔
175

176
        // share/unshare
177
        if shareOrUnshareRequired(serviceInstance) {
2✔
178
                return r.handleInstanceSharing(ctx, serviceInstance, smClient)
1✔
179
        }
1✔
180

181
        log.Info("No action required")
1✔
182
        return ctrl.Result{}, nil
1✔
183
}
184

185
func (r *ServiceInstanceReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
186
        return ctrl.NewControllerManagedBy(mgr).
1✔
187
                For(&v1.ServiceInstance{}).
1✔
188
                WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](r.Config.RetryBaseDelay, r.Config.RetryMaxDelay)}).
1✔
189
                Complete(r)
1✔
190
}
1✔
191

192
func (r *ServiceInstanceReconciler) createInstance(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
193
        log := logutils.GetLogger(ctx)
1✔
194
        log.Info("Creating instance in SM")
1✔
195
        updateHashedSpecValue(serviceInstance)
1✔
196
        instanceParameters, err := r.buildSMRequestParameters(ctx, serviceInstance)
1✔
197
        if err != nil {
2✔
198
                // if parameters are invalid there is nothing we can do, the user should fix it according to the error message in the condition
1✔
199
                log.Error(err, "failed to parse instance parameters")
1✔
200
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, smClientTypes.CREATE, err)
1✔
201
        }
1✔
202

203
        provision, provisionErr := smClient.Provision(&smClientTypes.ServiceInstance{
1✔
204
                Name:          serviceInstance.Spec.ExternalName,
1✔
205
                ServicePlanID: serviceInstance.Spec.ServicePlanID,
1✔
206
                Parameters:    instanceParameters,
1✔
207
                Labels: smClientTypes.Labels{
1✔
208
                        common.NamespaceLabel: []string{serviceInstance.Namespace},
1✔
209
                        common.K8sNameLabel:   []string{serviceInstance.Name},
1✔
210
                        common.ClusterIDLabel: []string{r.Config.ClusterID},
1✔
211
                },
1✔
212
        }, serviceInstance.Spec.ServiceOfferingName, serviceInstance.Spec.ServicePlanName, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo), serviceInstance.Spec.DataCenter)
1✔
213

1✔
214
        if provisionErr != nil {
2✔
215
                log.Error(provisionErr, "failed to create service instance", "serviceOfferingName", serviceInstance.Spec.ServiceOfferingName,
1✔
216
                        "servicePlanName", serviceInstance.Spec.ServicePlanName)
1✔
217
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.CREATE, provisionErr)
1✔
218
        }
1✔
219

220
        serviceInstance.Status.InstanceID = provision.InstanceID
1✔
221
        serviceInstance.Status.SubaccountID = provision.SubaccountID
1✔
222
        if len(provision.Tags) > 0 {
2✔
223
                tags, err := getTags(provision.Tags)
1✔
224
                if err != nil {
1✔
225
                        log.Error(err, "failed to unmarshal tags")
×
226
                } else {
1✔
227
                        serviceInstance.Status.Tags = tags
1✔
228
                }
1✔
229
        }
230

231
        if provision.Location != "" {
2✔
232
                log.Info("Provision request is in progress (async)")
1✔
233
                serviceInstance.Status.OperationURL = provision.Location
1✔
234
                serviceInstance.Status.OperationType = smClientTypes.CREATE
1✔
235
                utils.SetInProgressConditions(ctx, smClientTypes.CREATE, "", serviceInstance, false)
1✔
236

1✔
237
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
238
        }
1✔
239

240
        log.Info(fmt.Sprintf("Instance provisioned successfully, instanceID: %s, subaccountID: %s", serviceInstance.Status.InstanceID,
1✔
241
                serviceInstance.Status.SubaccountID))
1✔
242
        utils.SetSuccessConditions(smClientTypes.CREATE, serviceInstance, false)
1✔
243
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
244
}
245

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

1✔
250
        instanceParameters, err := r.buildSMRequestParameters(ctx, serviceInstance)
1✔
251
        if err != nil {
2✔
252
                log.Error(err, "failed to parse instance parameters")
1✔
253
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, smClientTypes.UPDATE, err)
1✔
254
        }
1✔
255

256
        updateHashedSpecValue(serviceInstance)
1✔
257
        _, operationURL, err := smClient.UpdateInstance(serviceInstance.Status.InstanceID, &smClientTypes.ServiceInstance{
1✔
258
                Name:          serviceInstance.Spec.ExternalName,
1✔
259
                ServicePlanID: serviceInstance.Spec.ServicePlanID,
1✔
260
                Parameters:    instanceParameters,
1✔
261
        }, serviceInstance.Spec.ServiceOfferingName, serviceInstance.Spec.ServicePlanName, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo), serviceInstance.Spec.DataCenter)
1✔
262

1✔
263
        if err != nil {
2✔
264
                log.Error(err, fmt.Sprintf("failed to update service instance with ID %s", serviceInstance.Status.InstanceID))
1✔
265
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.UPDATE, err)
1✔
266
        }
1✔
267

268
        if operationURL != "" {
2✔
269
                log.Info(fmt.Sprintf("Update request accepted, operation URL: %s", operationURL))
1✔
270
                serviceInstance.Status.OperationURL = operationURL
1✔
271
                serviceInstance.Status.OperationType = smClientTypes.UPDATE
1✔
272
                utils.SetInProgressConditions(ctx, smClientTypes.UPDATE, "", serviceInstance, false)
1✔
273
                serviceInstance.Status.ForceReconcile = false
1✔
274
                if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
2✔
275
                        return ctrl.Result{}, err
1✔
276
                }
1✔
277

278
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
279
        }
280
        log.Info("Instance updated successfully")
1✔
281
        utils.SetSuccessConditions(smClientTypes.UPDATE, serviceInstance, false)
1✔
282
        serviceInstance.Status.ForceReconcile = false
1✔
283
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
284
}
285

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

1✔
289
        if controllerutil.ContainsFinalizer(serviceInstance, common.FinalizerName) {
2✔
290
                log.Info("instance has finalizer, deleting it from sm")
1✔
291
                smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
292
                if err != nil {
1✔
293
                        log.Error(err, "failed to get sm client")
×
294
                        return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, smClientTypes.DELETE, err)
×
295
                }
×
296
                if len(serviceInstance.Status.InstanceID) == 0 {
2✔
297
                        log.Info("No instance id found validating instance does not exists in SM before removing finalizer")
1✔
298
                        smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
299
                        if err != nil {
1✔
300
                                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.DELETE, err)
×
301
                        }
×
302
                        if smInstance != nil {
2✔
303
                                log.Info("instance exists in SM continue with deletion")
1✔
304
                                serviceInstance.Status.InstanceID = smInstance.ID
1✔
305
                                utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "delete after recovery", serviceInstance, false)
1✔
306
                                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
307
                        }
1✔
308
                        log.Info("instance does not exists in SM, removing finalizer")
1✔
309
                        return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName)
1✔
310
                }
311

312
                if len(serviceInstance.Status.OperationURL) > 0 && serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
313
                        // ongoing delete operation - poll status from SM
1✔
314
                        log.Info("instance deletion is already in progress, checking status")
1✔
315
                        return r.poll(ctx, serviceInstance)
1✔
316
                }
1✔
317

318
                log.Info(fmt.Sprintf("Deleting instance with id %v from SM", serviceInstance.Status.InstanceID))
1✔
319
                operationURL, deprovisionErr := smClient.Deprovision(serviceInstance.Status.InstanceID, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
320
                if deprovisionErr != nil {
2✔
321
                        return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.DELETE, deprovisionErr)
1✔
322
                }
1✔
323

324
                if operationURL != "" {
2✔
325
                        log.Info("Deleting instance async")
1✔
326
                        return r.handleAsyncDelete(ctx, serviceInstance, operationURL)
1✔
327
                }
1✔
328

329
                for key, secretName := range serviceInstance.Labels {
2✔
330
                        if strings.HasPrefix(key, common.InstanceSecretRefLabel) {
2✔
331
                                if err := utils.RemoveWatchForSecret(ctx, r.Client, types.NamespacedName{Name: secretName, Namespace: serviceInstance.Namespace}, string(serviceInstance.UID)); err != nil {
1✔
332
                                        log.Error(err, fmt.Sprintf("failed to unwatch secret %s", secretName))
×
333
                                        return ctrl.Result{}, err
×
334
                                }
×
335
                        }
336
                }
337

338
                serviceInstance.Status.InstanceID = ""
1✔
339
                if err := r.Client.Status().Update(ctx, serviceInstance); err != nil {
1✔
340
                        log.Error(err, "failed to update service instance status after deletion")
×
341
                        return ctrl.Result{}, err
×
342
                }
×
343
                log.Info("Instance was deleted successfully, removing finalizer")
1✔
344
                // remove our finalizer from the list and update it.
1✔
345
                return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName)
1✔
346
        }
347
        return ctrl.Result{}, nil
1✔
348
}
349

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

1✔
354
        if serviceInstance.GetShared() {
2✔
355
                log.Info("Service instance appears to be unshared, sharing the instance")
1✔
356
                err := smClient.ShareInstance(serviceInstance.Status.InstanceID, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
357
                if err != nil {
2✔
358
                        log.Error(err, "failed to share instance")
1✔
359
                        return utils.HandleInstanceSharingError(ctx, r.Client, serviceInstance, metav1.ConditionFalse, common.ShareFailed, err)
1✔
360
                }
1✔
361
                log.Info("instance shared successfully")
1✔
362
                utils.SetSharedCondition(serviceInstance, metav1.ConditionTrue, common.ShareSucceeded, "instance shared successfully")
1✔
363
        } else { //un-share
1✔
364
                log.Info("Service instance appears to be shared, un-sharing the instance")
1✔
365
                err := smClient.UnShareInstance(serviceInstance.Status.InstanceID, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
366
                if err != nil {
2✔
367
                        log.Error(err, "failed to un-share instance")
1✔
368
                        return utils.HandleInstanceSharingError(ctx, r.Client, serviceInstance, metav1.ConditionTrue, common.UnShareFailed, err)
1✔
369
                }
1✔
370
                log.Info("instance un-shared successfully")
1✔
371
                if serviceInstance.Spec.Shared != nil {
2✔
372
                        utils.SetSharedCondition(serviceInstance, metav1.ConditionFalse, common.UnShareSucceeded, "instance un-shared successfully")
1✔
373
                } else {
2✔
374
                        log.Info("removing Shared condition since shared is undefined in instance")
1✔
375
                        conditions := serviceInstance.GetConditions()
1✔
376
                        meta.RemoveStatusCondition(&conditions, common.ConditionShared)
1✔
377
                        serviceInstance.SetConditions(conditions)
1✔
378
                }
1✔
379
        }
380

381
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
382
}
383

384
func (r *ServiceInstanceReconciler) poll(ctx context.Context, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
385
        log := logutils.GetLogger(ctx)
1✔
386
        log.Info(fmt.Sprintf("instance resource is in progress, found operation url %s", serviceInstance.Status.OperationURL))
1✔
387
        smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
388
        if err != nil {
1✔
389
                log.Error(err, "failed to get sm client")
×
390
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, common.Unknown, err)
×
391
        }
×
392

393
        status, statusErr := smClient.Status(serviceInstance.Status.OperationURL, nil)
1✔
394
        if statusErr != nil {
1✔
395
                log.Info(fmt.Sprintf("failed to fetch operation, got error from SM: %s", statusErr.Error()), "operationURL", serviceInstance.Status.OperationURL)
×
396
                utils.SetInProgressConditions(ctx, serviceInstance.Status.OperationType, string(smClientTypes.INPROGRESS), serviceInstance, false)
×
397
                // if failed to read operation status we cleanup the status to trigger re-sync from SM
×
398
                freshStatus := v1.ServiceInstanceStatus{Conditions: serviceInstance.GetConditions()}
×
399
                if utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) {
×
400
                        freshStatus.InstanceID = serviceInstance.Status.InstanceID
×
401
                }
×
402
                serviceInstance.Status = freshStatus
×
403
                if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
×
404
                        log.Error(err, "failed to update status during polling")
×
405
                }
×
406
                return ctrl.Result{}, statusErr
×
407
        }
408

409
        if status == nil {
1✔
410
                log.Error(fmt.Errorf("last operation is nil"), fmt.Sprintf("polling %s returned nil", serviceInstance.Status.OperationURL))
×
411
                return ctrl.Result{}, fmt.Errorf("last operation is nil")
×
412
        }
×
413
        switch status.State {
1✔
414
        case smClientTypes.INPROGRESS:
1✔
415
                fallthrough
1✔
416
        case smClientTypes.PENDING:
1✔
417
                log.Info(fmt.Sprintf("operation %s %s is still in progress", serviceInstance.Status.OperationType, serviceInstance.Status.OperationURL))
1✔
418
                if len(status.Description) > 0 {
1✔
419
                        log.Info(fmt.Sprintf("last operation description is '%s'", status.Description))
×
420
                        utils.SetInProgressConditions(ctx, status.Type, status.Description, serviceInstance, true)
×
421
                        if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
×
422
                                log.Error(err, "unable to update ServiceInstance polling description")
×
423
                                return ctrl.Result{}, err
×
424
                        }
×
425
                }
426
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
427
        case smClientTypes.FAILED:
1✔
428
                errMsg := getErrorMsgFromLastOperation(status)
1✔
429
                log.Info(fmt.Sprintf("operation %s %s failed, error: %s", serviceInstance.Status.OperationType, serviceInstance.Status.OperationURL, errMsg))
1✔
430
                utils.SetFailureConditions(status.Type, errMsg, serviceInstance, true)
1✔
431
                serviceInstance.Status.OperationURL = ""
1✔
432
                serviceInstance.Status.OperationType = ""
1✔
433
                if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
1✔
434
                        return ctrl.Result{}, err
×
435
                }
×
436
                return ctrl.Result{}, errors.New(errMsg)
1✔
437
        case smClientTypes.SUCCEEDED:
1✔
438
                log.Info(fmt.Sprintf("operation %s %s completed succefully", serviceInstance.Status.OperationType, serviceInstance.Status.OperationURL))
1✔
439
                if serviceInstance.Status.OperationType == smClientTypes.CREATE {
2✔
440
                        smInstance, err := smClient.GetInstanceByID(serviceInstance.Status.InstanceID, nil)
1✔
441
                        if err != nil {
1✔
442
                                log.Error(err, fmt.Sprintf("instance %s succeeded but could not fetch it from SM", serviceInstance.Status.InstanceID))
×
443
                                return ctrl.Result{}, err
×
444
                        }
×
445
                        if len(smInstance.Labels["subaccount_id"]) > 0 {
2✔
446
                                serviceInstance.Status.SubaccountID = smInstance.Labels["subaccount_id"][0]
1✔
447
                        }
1✔
448
                        serviceInstance.Status.Ready = metav1.ConditionTrue
1✔
449
                } else if serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
450
                        log.Info(fmt.Sprintf("instance %s deleted successfully from sm, removing finalizer", serviceInstance.Status.InstanceID))
1✔
451
                        if err := utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName); err != nil {
1✔
452
                                return ctrl.Result{}, err
×
453
                        }
×
454
                        serviceInstance.Status.OperationURL = ""
1✔
455
                        serviceInstance.Status.OperationType = ""
1✔
456
                        serviceInstance.Status.InstanceID = ""
1✔
457
                        return ctrl.Result{RequeueAfter: time.Second}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
458
                }
459
                utils.SetSuccessConditions(status.Type, serviceInstance, true)
1✔
460
        }
461

462
        serviceInstance.Status.OperationURL = ""
1✔
463
        serviceInstance.Status.OperationType = ""
1✔
464

1✔
465
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
466
}
467

468
func (r *ServiceInstanceReconciler) handleAsyncDelete(ctx context.Context, serviceInstance *v1.ServiceInstance, opURL string) (ctrl.Result, error) {
1✔
469
        serviceInstance.Status.OperationURL = opURL
1✔
470
        serviceInstance.Status.OperationType = smClientTypes.DELETE
1✔
471
        utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "", serviceInstance, false)
1✔
472

1✔
473
        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
474
}
1✔
475

476
func (r *ServiceInstanceReconciler) getInstanceForRecovery(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (*smClientTypes.ServiceInstance, error) {
1✔
477
        log := logutils.GetLogger(ctx)
1✔
478
        parameters := sm.Parameters{
1✔
479
                FieldQuery: []string{
1✔
480
                        fmt.Sprintf("name eq '%s'", serviceInstance.Spec.ExternalName),
1✔
481
                        fmt.Sprintf("context/clusterid eq '%s'", r.Config.ClusterID),
1✔
482
                        fmt.Sprintf("context/namespace eq '%s'", serviceInstance.Namespace)},
1✔
483
                LabelQuery: []string{
1✔
484
                        fmt.Sprintf("%s eq '%s'", common.K8sNameLabel, serviceInstance.Name)},
1✔
485
                GeneralParams: []string{"attach_last_operations=true"},
1✔
486
        }
1✔
487

1✔
488
        instances, err := smClient.ListInstances(&parameters)
1✔
489
        if err != nil {
1✔
490
                log.Error(err, "failed to list instances in SM")
×
491
                return nil, err
×
492
        }
×
493

494
        if instances != nil && len(instances.ServiceInstances) > 0 {
2✔
495
                return &instances.ServiceInstances[0], nil
1✔
496
        }
1✔
497
        log.Info("instance not found in SM")
1✔
498
        return nil, nil
1✔
499
}
500

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

1✔
504
        log.Info(fmt.Sprintf("found existing instance in SM with id %s, updating status", smInstance.ID))
1✔
505
        updateHashedSpecValue(k8sInstance)
1✔
506
        if smInstance.Ready {
2✔
507
                k8sInstance.Status.Ready = metav1.ConditionTrue
1✔
508
        }
1✔
509
        if smInstance.Shared {
1✔
510
                utils.SetSharedCondition(k8sInstance, metav1.ConditionTrue, common.ShareSucceeded, "Instance shared successfully")
×
511
        }
×
512
        k8sInstance.Status.InstanceID = smInstance.ID
1✔
513
        k8sInstance.Status.OperationURL = ""
1✔
514
        k8sInstance.Status.OperationType = ""
1✔
515
        tags, err := getOfferingTags(smClient, smInstance.ServicePlanID)
1✔
516
        if err != nil {
2✔
517
                log.Error(err, "could not recover offering tags")
1✔
518
        }
1✔
519
        if len(tags) > 0 {
1✔
520
                k8sInstance.Status.Tags = tags
×
521
        }
×
522

523
        instanceState := smClientTypes.SUCCEEDED
1✔
524
        operationType := smClientTypes.CREATE
1✔
525
        description := ""
1✔
526
        if smInstance.LastOperation != nil {
2✔
527
                instanceState = smInstance.LastOperation.State
1✔
528
                operationType = smInstance.LastOperation.Type
1✔
529
                description = smInstance.LastOperation.Description
1✔
530
        } else if !smInstance.Ready {
3✔
531
                instanceState = smClientTypes.FAILED
1✔
532
        }
1✔
533

534
        switch instanceState {
1✔
535
        case smClientTypes.PENDING:
1✔
536
                fallthrough
1✔
537
        case smClientTypes.INPROGRESS:
1✔
538
                k8sInstance.Status.OperationURL = sm.BuildOperationURL(smInstance.LastOperation.ID, smInstance.ID, smClientTypes.ServiceInstancesURL)
1✔
539
                k8sInstance.Status.OperationType = smInstance.LastOperation.Type
1✔
540
                k8sInstance.Status.InstanceID = smInstance.ID
1✔
541
                utils.SetInProgressConditions(ctx, smInstance.LastOperation.Type, smInstance.LastOperation.Description, k8sInstance, false)
1✔
542
        case smClientTypes.SUCCEEDED:
1✔
543
                utils.SetSuccessConditions(operationType, k8sInstance, false)
1✔
544
        case smClientTypes.FAILED:
1✔
545
                utils.SetFailureConditions(operationType, description, k8sInstance, false)
1✔
546
        }
547

548
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, k8sInstance)
1✔
549
}
550

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

569
                        if err := utils.AddWatchForSecretIfNeeded(ctx, r.Client, secret, string(serviceInstance.UID)); err != nil {
1✔
570
                                log.Error(err, fmt.Sprintf("failed to mark secret for watch %s", secret.Name))
×
571
                                return nil, err
×
572
                        }
×
573
                }
574
        }
575

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

598
        return instanceParameters, nil
1✔
599
}
600

601
func (r *ServiceInstanceReconciler) maintainFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
602
        log := logutils.GetLogger(ctx)
1✔
603

1✔
604
        if serviceInstance.IsSubscribedToParamSecretsChanges() {
2✔
605
                log.Info("instance is in final state, WatchParametersFromChanges is true, validating that all parameters secrets are watched")
1✔
606
                for _, param := range serviceInstance.Spec.ParametersFrom {
2✔
607
                        if param.SecretKeyRef != nil {
2✔
608
                                secret := &corev1.Secret{}
1✔
609
                                if err := r.Get(ctx, types.NamespacedName{Name: param.SecretKeyRef.Name, Namespace: serviceInstance.Namespace}, secret); err != nil {
1✔
NEW
610
                                        log.Error(err, fmt.Sprintf("failed to get secret %s", param.SecretKeyRef.Name))
×
NEW
611
                                        return ctrl.Result{}, err
×
NEW
612
                                }
×
613
                                if err := utils.AddWatchForSecretIfNeeded(ctx, r.Client, secret, string(serviceInstance.UID)); err != nil {
1✔
NEW
614
                                        log.Error(err, fmt.Sprintf("failed to mark secret for watch %s", param.SecretKeyRef.Name))
×
NEW
615
                                        return ctrl.Result{}, err
×
NEW
616
                                }
×
617
                        }
618
                }
619
        }
620

621
        if len(serviceInstance.Status.HashedSpec) == 0 {
2✔
622
                log.Info("instance is missing HashedSpec value, updating it")
1✔
623
                updateHashedSpecValue(serviceInstance)
1✔
624
                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
625
        }
1✔
626
        return ctrl.Result{}, nil
1✔
627
}
628

629
func isFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) bool {
1✔
630
        log := logutils.GetLogger(ctx)
1✔
631

1✔
632
        if !serviceInstanceReady(serviceInstance) {
2✔
633
                return false
1✔
634
        }
1✔
635

636
        if serviceInstance.Status.ForceReconcile {
2✔
637
                log.Info("instance is not in final state, ForceReconcile is true")
1✔
638
                return false
1✔
639
        }
1✔
640

641
        observedGen := common.GetObservedGeneration(serviceInstance)
1✔
642
        if serviceInstance.Generation != observedGen {
2✔
643
                log.Info(fmt.Sprintf("instance is not in final state, generation: %d, observedGen: %d", serviceInstance.Generation, observedGen))
1✔
644
                return false
1✔
645
        }
1✔
646

647
        if shareOrUnshareRequired(serviceInstance) {
2✔
648
                log.Info("instance is not in final state, need to sync sharing status")
1✔
649
                if len(serviceInstance.Status.HashedSpec) == 0 {
1✔
650
                        updateHashedSpecValue(serviceInstance)
×
651
                }
×
652
                return false
1✔
653
        }
654

655
        log.Info(fmt.Sprintf("instance is in final state (generation: %d)", serviceInstance.Generation))
1✔
656
        return true
1✔
657
}
658

659
func updateRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
660
        //update is not supported for failed instances (this can occur when instance creation was asynchronously)
1✔
661
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
1✔
662
                return false
×
663
        }
×
664

665
        if serviceInstance.Status.ForceReconcile {
2✔
666
                return true
1✔
667
        }
1✔
668

669
        cond := meta.FindStatusCondition(serviceInstance.Status.Conditions, common.ConditionSucceeded)
1✔
670
        if cond != nil && cond.Reason == common.UpdateInProgress { //in case of transient error occurred
1✔
671
                return true
×
672
        }
×
673

674
        return serviceInstance.GetSpecHash() != serviceInstance.Status.HashedSpec
1✔
675
}
676

677
func shareOrUnshareRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
678
        //relevant only for non-shared instances - sharing instance is possible only for usable instances
1✔
679
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
1✔
680
                return false
×
681
        }
×
682

683
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), common.ConditionShared)
1✔
684
        if sharedCondition == nil {
2✔
685
                return serviceInstance.GetShared()
1✔
686
        }
1✔
687

688
        if sharedCondition.Reason == common.ShareNotSupported {
2✔
689
                return false
1✔
690
        }
1✔
691

692
        if sharedCondition.Status == metav1.ConditionFalse {
2✔
693
                // instance does not appear to be shared, should share it if shared is requested
1✔
694
                return serviceInstance.GetShared()
1✔
695
        }
1✔
696

697
        // instance appears to be shared, should unshare it if shared is not requested
698
        return !serviceInstance.GetShared()
1✔
699
}
700

701
func getOfferingTags(smClient sm.Client, planID string) ([]string, error) {
1✔
702
        planQuery := &sm.Parameters{
1✔
703
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", planID)},
1✔
704
        }
1✔
705
        plans, err := smClient.ListPlans(planQuery)
1✔
706
        if err != nil {
1✔
707
                return nil, err
×
708
        }
×
709

710
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
711
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
712
        }
1✔
713

714
        offeringQuery := &sm.Parameters{
×
715
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
716
        }
×
717

×
718
        offerings, err := smClient.ListOfferings(offeringQuery)
×
719
        if err != nil {
×
720
                return nil, err
×
721
        }
×
722
        if offerings == nil || len(offerings.ServiceOfferings) != 1 {
×
723
                return nil, fmt.Errorf("could not find offering with id %s", plans.ServicePlans[0].ServiceOfferingID)
×
724
        }
×
725

726
        var tags []string
×
727
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
728
                return nil, err
×
729
        }
×
730
        return tags, nil
×
731
}
732

733
func getTags(tags []byte) ([]string, error) {
1✔
734
        var tagsArr []string
1✔
735
        if err := json.Unmarshal(tags, &tagsArr); err != nil {
1✔
736
                return nil, err
×
737
        }
×
738
        return tagsArr, nil
1✔
739
}
740

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

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

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

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

764
func shouldInstanceBeDeleted(serviceInstance *v1.ServiceInstance) bool {
1✔
765
        return utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) ||
1✔
766
                (len(serviceInstance.Status.OperationURL) == 0 && len(serviceInstance.Status.InstanceID) > 0 && serviceInstance.Status.Ready == metav1.ConditionFalse) //async provision failed
1✔
767
}
1✔
768

769
type SecretPredicate struct {
770
        predicate.Funcs
771
}
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