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

SAP / sap-btp-service-operator / 20892495287

11 Jan 2026 08:53AM UTC coverage: 78.346% (-0.09%) from 78.436%
20892495287

Pull #593

github

kerenlahav
review
Pull Request #593: validate resource exists

55 of 82 new or added lines in 4 files covered. (67.07%)

1 existing line in 1 file now uncovered.

2786 of 3556 relevant lines covered (78.35%)

0.88 hits per line

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

80.89
/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
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
30

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

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

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

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

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

48
        "github.com/google/uuid"
49

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

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

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

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

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

1✔
90
        if utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) {
2✔
91
                return r.deleteInstance(ctx, serviceInstance)
1✔
92
        }
1✔
93

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

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

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

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

145
        if isFinalState(ctx, serviceInstance) {
2✔
146
                if len(serviceInstance.Status.HashedSpec) == 0 {
2✔
147
                        updateHashedSpecValue(serviceInstance)
1✔
148
                        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
149
                }
1✔
150

151
                return ctrl.Result{}, nil
1✔
152
        }
153

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

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

172
                // if instance was not recovered then create new instance
173
                return r.createInstance(ctx, smClient, serviceInstance)
1✔
174
        }
175

176
        if updateRequired(serviceInstance) {
2✔
177
                return r.updateInstance(ctx, smClient, serviceInstance)
1✔
178
        }
1✔
179

180
        // share/unshare
181
        if shareOrUnshareRequired(serviceInstance) {
2✔
182
                return r.handleInstanceSharing(ctx, serviceInstance, smClient)
1✔
183
        }
1✔
184

185
        log.Info("No action required")
1✔
186
        return ctrl.Result{}, nil
1✔
187
}
188

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

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

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

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

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

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

1✔
241
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
242
        }
1✔
243

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

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

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

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

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

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

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

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

1✔
293
        log.Info("deleting instance")
1✔
294
        if controllerutil.ContainsFinalizer(serviceInstance, common.FinalizerName) {
2✔
295
                for key, secretName := range serviceInstance.Labels {
2✔
296
                        if strings.HasPrefix(key, common.InstanceSecretRefLabel) {
2✔
297
                                if err := utils.RemoveWatchForSecret(ctx, r.Client, types.NamespacedName{Name: secretName, Namespace: serviceInstance.Namespace}, string(serviceInstance.UID)); err != nil {
1✔
298
                                        log.Error(err, fmt.Sprintf("failed to unwatch secret %s", secretName))
×
299
                                        return ctrl.Result{}, err
×
300
                                }
×
301
                        }
302
                }
303

304
                smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
305
                if err != nil {
1✔
306
                        log.Error(err, "failed to get sm client")
×
307
                        return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, smClientTypes.DELETE, err)
×
308
                }
×
309
                if len(serviceInstance.Status.InstanceID) == 0 {
2✔
310
                        log.Info("No instance id found validating instance does not exists in SM before removing finalizer")
1✔
311
                        smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
312
                        if err != nil {
1✔
313
                                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.DELETE, err)
×
314
                        }
×
315
                        if smInstance != nil {
2✔
316
                                log.Info("instance exists in SM continue with deletion")
1✔
317
                                serviceInstance.Status.InstanceID = smInstance.ID
1✔
318
                                utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "delete after recovery", serviceInstance, false)
1✔
319
                                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
320
                        }
1✔
321
                        log.Info("instance does not exists in SM, removing finalizer")
1✔
322
                        return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName)
1✔
323
                }
324

325
                if len(serviceInstance.Status.OperationURL) > 0 && serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
326
                        // ongoing delete operation - poll status from SM
1✔
327
                        return r.poll(ctx, serviceInstance)
1✔
328
                }
1✔
329

330
                log.Info(fmt.Sprintf("Deleting instance with id %v from SM", serviceInstance.Status.InstanceID))
1✔
331
                operationURL, deprovisionErr := smClient.Deprovision(serviceInstance.Status.InstanceID, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
332
                if deprovisionErr != nil {
2✔
333
                        return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.DELETE, deprovisionErr)
1✔
334
                }
1✔
335

336
                if operationURL != "" {
2✔
337
                        log.Info("Deleting instance async")
1✔
338
                        return r.handleAsyncDelete(ctx, serviceInstance, operationURL)
1✔
339
                }
1✔
340

341
                log.Info("Instance was deleted successfully, removing finalizer")
1✔
342
                // remove our finalizer from the list and update it.
1✔
343
                return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName)
1✔
344
        }
345
        return ctrl.Result{}, nil
1✔
346
}
347

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

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

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

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

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

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

456
        serviceInstance.Status.OperationURL = ""
1✔
457
        serviceInstance.Status.OperationType = ""
1✔
458

1✔
459
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
460
}
461

462
func (r *ServiceInstanceReconciler) handleAsyncDelete(ctx context.Context, serviceInstance *v1.ServiceInstance, opURL string) (ctrl.Result, error) {
1✔
463
        serviceInstance.Status.OperationURL = opURL
1✔
464
        serviceInstance.Status.OperationType = smClientTypes.DELETE
1✔
465
        utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "", serviceInstance, false)
1✔
466

1✔
467
        if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
1✔
468
                return ctrl.Result{}, err
×
469
        }
×
470

471
        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
472
}
473

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

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

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

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

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

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

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

545
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, k8sInstance)
1✔
546
}
547

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

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

571
                }
572
        }
573

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

596
        return instanceParameters, nil
1✔
597
}
598

599
func isFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) bool {
1✔
600
        log := logutils.GetLogger(ctx)
1✔
601

1✔
602
        if serviceInstance.Status.ForceReconcile {
2✔
603
                log.Info("instance is not in final state, ForceReconcile is true")
1✔
604
                return false
1✔
605
        }
1✔
606

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

613
        if utils.ShouldRetryOperation(serviceInstance) {
2✔
614
                log.Info("instance is not in final state, last operation failed, retrying")
1✔
615
                return false
1✔
616
        }
1✔
617

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

712
func updateHashedSpecValue(serviceInstance *v1.ServiceInstance) {
1✔
713
        serviceInstance.Status.HashedSpec = serviceInstance.GetSpecHash()
1✔
714
}
1✔
715

716
func getErrorMsgFromLastOperation(status *smClientTypes.Operation) string {
1✔
717
        errMsg := "async operation error"
1✔
718
        if status == nil || len(status.Errors) == 0 {
1✔
719
                return errMsg
×
720
        }
×
721
        var errMap map[string]interface{}
1✔
722

1✔
723
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
724
                return errMsg
×
725
        }
×
726

727
        if description, found := errMap["description"]; found {
2✔
728
                if descStr, ok := description.(string); ok {
2✔
729
                        errMsg = descStr
1✔
730
                }
1✔
731
        }
732
        return errMsg
1✔
733
}
734

735
type SecretPredicate struct {
736
        predicate.Funcs
737
}
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