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

SAP / sap-btp-service-operator / 20456524133

23 Dec 2025 09:10AM UTC coverage: 78.133% (-0.3%) from 78.422%
20456524133

Pull #593

github

kerenlahav
check if instance exists
Pull Request #593: check if instance exists

4 of 23 new or added lines in 1 file covered. (17.39%)

2 existing lines in 1 file now uncovered.

2737 of 3503 relevant lines covered (78.13%)

0.88 hits per line

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

77.92
/controllers/serviceinstance_controller.go
1
/*
2

3

4
Licensed under the Apache License, Version 2.0 (the "License");
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    http://www.apache.org/licenses/LICENSE-2.0
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
package controllers
18

19
import (
20
        "context"
21
        "encoding/json"
22
        "fmt"
23
        "net/http"
24
        "strings"
25

26
        "github.com/SAP/sap-btp-service-operator/internal/utils/logutils"
27
        "github.com/pkg/errors"
28
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
29

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

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

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

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

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

47
        "github.com/google/uuid"
48

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

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

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

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

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

1✔
89
        smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
90
        if err != nil {
1✔
NEW
91
                log.Error(err, "failed to get sm client")
×
NEW
92
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, common.Unknown, err)
×
NEW
93
        }
×
94

95
        if len(serviceInstance.Status.InstanceID) > 0 {
2✔
96
                if _, err := smClient.GetInstanceByID(serviceInstance.Status.InstanceID, nil); err != nil {
1✔
NEW
97
                        var smError *sm.ServiceManagerError
×
NEW
98
                        if ok := errors.As(err, &smError); ok {
×
NEW
99
                                if smError.StatusCode == http.StatusNotFound {
×
NEW
100
                                        log.Info(fmt.Sprintf("instance %s not found in SM", serviceInstance.Status.InstanceID))
×
NEW
101
                                        condition := metav1.Condition{
×
NEW
102
                                                Type:               common.ConditionReady,
×
NEW
103
                                                Status:             metav1.ConditionFalse,
×
NEW
104
                                                ObservedGeneration: serviceInstance.Generation,
×
NEW
105
                                                Reason:             common.Failed,
×
NEW
106
                                                Message:            fmt.Sprintf("instance %s not found in Service Manager", serviceInstance.Status.InstanceID),
×
NEW
107
                                        }
×
NEW
108
                                        meta.SetStatusCondition(&serviceInstance.Status.Conditions, condition)
×
NEW
109
                                        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
×
NEW
110
                                }
×
111
                        }
NEW
112
                        log.Error(err, fmt.Sprintf("failed to get instance %s from SM", serviceInstance.Status.InstanceID))
×
NEW
113
                        return ctrl.Result{}, err
×
114
                }
115
        }
116

117
        if utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) {
2✔
118
                return r.deleteInstance(ctx, serviceInstance)
1✔
119
        }
1✔
120
        if len(serviceInstance.GetConditions()) == 0 {
2✔
121
                err := utils.InitConditions(ctx, r.Client, serviceInstance)
1✔
122
                if err != nil {
1✔
123
                        return ctrl.Result{}, err
×
124
                }
×
125
        }
126

127
        if len(serviceInstance.Status.OperationURL) > 0 {
2✔
128
                // ongoing operation - poll status from SM
1✔
129
                return r.poll(ctx, serviceInstance)
1✔
130
        }
1✔
131

132
        if isFinalState(ctx, serviceInstance) {
2✔
133
                if len(serviceInstance.Status.HashedSpec) == 0 {
2✔
134
                        updateHashedSpecValue(serviceInstance)
1✔
135
                        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
136
                }
1✔
137

138
                return ctrl.Result{}, nil
1✔
139
        }
140

141
        if controllerutil.AddFinalizer(serviceInstance, common.FinalizerName) {
2✔
142
                log.Info(fmt.Sprintf("added finalizer '%s' to service instance", common.FinalizerName))
1✔
143
                if err := r.Client.Update(ctx, serviceInstance); err != nil {
1✔
144
                        return ctrl.Result{}, err
×
145
                }
×
146
        }
147

148
        if serviceInstance.Status.InstanceID == "" {
2✔
149
                log.Info("Instance ID is empty, checking if instance exist in SM")
1✔
150
                smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
151
                if err != nil {
1✔
152
                        log.Error(err, "failed to check instance recovery")
×
153
                        return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.CREATE, err)
×
154
                }
×
155
                if smInstance != nil {
2✔
156
                        return r.recover(ctx, smClient, serviceInstance, smInstance)
1✔
157
                }
1✔
158

159
                // if instance was not recovered then create new instance
160
                return r.createInstance(ctx, smClient, serviceInstance)
1✔
161
        }
162

163
        // Update
164
        if updateRequired(serviceInstance) {
2✔
165
                return r.updateInstance(ctx, smClient, serviceInstance)
1✔
166
        }
1✔
167

168
        // share/unshare
169
        if shareOrUnshareRequired(serviceInstance) {
2✔
170
                return r.handleInstanceSharing(ctx, serviceInstance, smClient)
1✔
171
        }
1✔
172

173
        log.Info("No action required")
1✔
174
        return ctrl.Result{}, nil
1✔
175
}
176

177
func (r *ServiceInstanceReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
178
        return ctrl.NewControllerManagedBy(mgr).
1✔
179
                For(&v1.ServiceInstance{}).
1✔
180
                WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](r.Config.RetryBaseDelay, r.Config.RetryMaxDelay)}).
1✔
181
                Complete(r)
1✔
182
}
1✔
183

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

195
        provision, provisionErr := smClient.Provision(&smClientTypes.ServiceInstance{
1✔
196
                Name:          serviceInstance.Spec.ExternalName,
1✔
197
                ServicePlanID: serviceInstance.Spec.ServicePlanID,
1✔
198
                Parameters:    instanceParameters,
1✔
199
                Labels: smClientTypes.Labels{
1✔
200
                        common.NamespaceLabel: []string{serviceInstance.Namespace},
1✔
201
                        common.K8sNameLabel:   []string{serviceInstance.Name},
1✔
202
                        common.ClusterIDLabel: []string{r.Config.ClusterID},
1✔
203
                },
1✔
204
        }, serviceInstance.Spec.ServiceOfferingName, serviceInstance.Spec.ServicePlanName, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo), serviceInstance.Spec.DataCenter)
1✔
205

1✔
206
        if provisionErr != nil {
2✔
207
                log.Error(provisionErr, "failed to create service instance", "serviceOfferingName", serviceInstance.Spec.ServiceOfferingName,
1✔
208
                        "servicePlanName", serviceInstance.Spec.ServicePlanName)
1✔
209
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.CREATE, provisionErr)
1✔
210
        }
1✔
211

212
        serviceInstance.Status.InstanceID = provision.InstanceID
1✔
213
        serviceInstance.Status.SubaccountID = provision.SubaccountID
1✔
214
        if len(provision.Tags) > 0 {
2✔
215
                tags, err := getTags(provision.Tags)
1✔
216
                if err != nil {
1✔
217
                        log.Error(err, "failed to unmarshal tags")
×
218
                } else {
1✔
219
                        serviceInstance.Status.Tags = tags
1✔
220
                }
1✔
221
        }
222

223
        if provision.Location != "" {
2✔
224
                log.Info("Provision request is in progress (async)")
1✔
225
                serviceInstance.Status.OperationURL = provision.Location
1✔
226
                serviceInstance.Status.OperationType = smClientTypes.CREATE
1✔
227
                utils.SetInProgressConditions(ctx, smClientTypes.CREATE, "", serviceInstance, false)
1✔
228

1✔
229
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
230
        }
1✔
231

232
        log.Info(fmt.Sprintf("Instance provisioned successfully, instanceID: %s, subaccountID: %s", serviceInstance.Status.InstanceID,
1✔
233
                serviceInstance.Status.SubaccountID))
1✔
234
        utils.SetSuccessConditions(smClientTypes.CREATE, serviceInstance, false)
1✔
235
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
236
}
237

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

1✔
242
        instanceParameters, err := r.buildSMRequestParameters(ctx, serviceInstance)
1✔
243
        if err != nil {
2✔
244
                log.Error(err, "failed to parse instance parameters")
1✔
245
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, smClientTypes.UPDATE, err)
1✔
246
        }
1✔
247

248
        updateHashedSpecValue(serviceInstance)
1✔
249
        _, operationURL, err := smClient.UpdateInstance(serviceInstance.Status.InstanceID, &smClientTypes.ServiceInstance{
1✔
250
                Name:          serviceInstance.Spec.ExternalName,
1✔
251
                ServicePlanID: serviceInstance.Spec.ServicePlanID,
1✔
252
                Parameters:    instanceParameters,
1✔
253
        }, serviceInstance.Spec.ServiceOfferingName, serviceInstance.Spec.ServicePlanName, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo), serviceInstance.Spec.DataCenter)
1✔
254

1✔
255
        if err != nil {
2✔
256
                log.Error(err, fmt.Sprintf("failed to update service instance with ID %s", serviceInstance.Status.InstanceID))
1✔
257
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.UPDATE, err)
1✔
258
        }
1✔
259

260
        if operationURL != "" {
2✔
261
                log.Info(fmt.Sprintf("Update request accepted, operation URL: %s", operationURL))
1✔
262
                serviceInstance.Status.OperationURL = operationURL
1✔
263
                serviceInstance.Status.OperationType = smClientTypes.UPDATE
1✔
264
                utils.SetInProgressConditions(ctx, smClientTypes.UPDATE, "", serviceInstance, false)
1✔
265
                serviceInstance.Status.ForceReconcile = false
1✔
266
                if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
2✔
267
                        return ctrl.Result{}, err
1✔
268
                }
1✔
269

270
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
271
        }
272
        log.Info("Instance updated successfully")
1✔
273
        utils.SetSuccessConditions(smClientTypes.UPDATE, serviceInstance, false)
1✔
274
        serviceInstance.Status.ForceReconcile = false
1✔
275
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
276
}
277

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

1✔
281
        log.Info("deleting instance")
1✔
282
        if controllerutil.ContainsFinalizer(serviceInstance, common.FinalizerName) {
2✔
283
                for key, secretName := range serviceInstance.Labels {
2✔
284
                        if strings.HasPrefix(key, common.InstanceSecretRefLabel) {
2✔
285
                                if err := utils.RemoveWatchForSecret(ctx, r.Client, types.NamespacedName{Name: secretName, Namespace: serviceInstance.Namespace}, string(serviceInstance.UID)); err != nil {
1✔
286
                                        log.Error(err, fmt.Sprintf("failed to unwatch secret %s", secretName))
×
287
                                        return ctrl.Result{}, err
×
288
                                }
×
289
                        }
290
                }
291

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

313
                if len(serviceInstance.Status.OperationURL) > 0 && serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
314
                        // ongoing delete operation - poll status from SM
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
                log.Info("Instance was deleted successfully, removing finalizer")
1✔
330
                // remove our finalizer from the list and update it.
1✔
331
                return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName)
1✔
332
        }
333
        return ctrl.Result{}, nil
1✔
334
}
335

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

1✔
340
        if serviceInstance.GetShared() {
2✔
341
                log.Info("Service instance appears to be unshared, sharing the instance")
1✔
342
                err := smClient.ShareInstance(serviceInstance.Status.InstanceID, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
343
                if err != nil {
2✔
344
                        log.Error(err, "failed to share instance")
1✔
345
                        return utils.HandleInstanceSharingError(ctx, r.Client, serviceInstance, metav1.ConditionFalse, common.ShareFailed, err)
1✔
346
                }
1✔
347
                log.Info("instance shared successfully")
1✔
348
                utils.SetSharedCondition(serviceInstance, metav1.ConditionTrue, common.ShareSucceeded, "instance shared successfully")
1✔
349
        } else { //un-share
1✔
350
                log.Info("Service instance appears to be shared, un-sharing the instance")
1✔
351
                err := smClient.UnShareInstance(serviceInstance.Status.InstanceID, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
352
                if err != nil {
2✔
353
                        log.Error(err, "failed to un-share instance")
1✔
354
                        return utils.HandleInstanceSharingError(ctx, r.Client, serviceInstance, metav1.ConditionTrue, common.UnShareFailed, err)
1✔
355
                }
1✔
356
                log.Info("instance un-shared successfully")
1✔
357
                if serviceInstance.Spec.Shared != nil {
2✔
358
                        utils.SetSharedCondition(serviceInstance, metav1.ConditionFalse, common.UnShareSucceeded, "instance un-shared successfully")
1✔
359
                } else {
2✔
360
                        log.Info("removing Shared condition since shared is undefined in instance")
1✔
361
                        conditions := serviceInstance.GetConditions()
1✔
362
                        meta.RemoveStatusCondition(&conditions, common.ConditionShared)
1✔
363
                        serviceInstance.SetConditions(conditions)
1✔
364
                }
1✔
365
        }
366

367
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
368
}
369

370
func (r *ServiceInstanceReconciler) poll(ctx context.Context, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
371
        log := logutils.GetLogger(ctx)
1✔
372
        log.Info(fmt.Sprintf("resource is in progress, found operation url %s", serviceInstance.Status.OperationURL))
1✔
373
        smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
374
        if err != nil {
1✔
375
                log.Error(err, "failed to get sm client")
×
376
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, common.Unknown, err)
×
377
        }
×
378

379
        status, statusErr := smClient.Status(serviceInstance.Status.OperationURL, nil)
1✔
380
        if statusErr != nil {
1✔
381
                log.Info(fmt.Sprintf("failed to fetch operation, got error from SM: %s", statusErr.Error()), "operationURL", serviceInstance.Status.OperationURL)
×
382
                utils.SetInProgressConditions(ctx, serviceInstance.Status.OperationType, string(smClientTypes.INPROGRESS), serviceInstance, false)
×
383
                // if failed to read operation status we cleanup the status to trigger re-sync from SM
×
384
                freshStatus := v1.ServiceInstanceStatus{Conditions: serviceInstance.GetConditions()}
×
385
                if utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) {
×
386
                        freshStatus.InstanceID = serviceInstance.Status.InstanceID
×
387
                }
×
388
                serviceInstance.Status = freshStatus
×
389
                if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
×
390
                        log.Error(err, "failed to update status during polling")
×
391
                }
×
392
                return ctrl.Result{}, statusErr
×
393
        }
394

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

444
        serviceInstance.Status.OperationURL = ""
1✔
445
        serviceInstance.Status.OperationType = ""
1✔
446

1✔
447
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
448
}
449

450
func (r *ServiceInstanceReconciler) handleAsyncDelete(ctx context.Context, serviceInstance *v1.ServiceInstance, opURL string) (ctrl.Result, error) {
1✔
451
        serviceInstance.Status.OperationURL = opURL
1✔
452
        serviceInstance.Status.OperationType = smClientTypes.DELETE
1✔
453
        utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "", serviceInstance, false)
1✔
454

1✔
455
        if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
1✔
456
                return ctrl.Result{}, err
×
457
        }
×
458

459
        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
460
}
461

462
func (r *ServiceInstanceReconciler) getInstanceForRecovery(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (*smClientTypes.ServiceInstance, error) {
1✔
463
        log := logutils.GetLogger(ctx)
1✔
464
        parameters := sm.Parameters{
1✔
465
                FieldQuery: []string{
1✔
466
                        fmt.Sprintf("name eq '%s'", serviceInstance.Spec.ExternalName),
1✔
467
                        fmt.Sprintf("context/clusterid eq '%s'", r.Config.ClusterID),
1✔
468
                        fmt.Sprintf("context/namespace eq '%s'", serviceInstance.Namespace)},
1✔
469
                LabelQuery: []string{
1✔
470
                        fmt.Sprintf("%s eq '%s'", common.K8sNameLabel, serviceInstance.Name)},
1✔
471
                GeneralParams: []string{"attach_last_operations=true"},
1✔
472
        }
1✔
473

1✔
474
        instances, err := smClient.ListInstances(&parameters)
1✔
475
        if err != nil {
1✔
476
                log.Error(err, "failed to list instances in SM")
×
477
                return nil, err
×
478
        }
×
479

480
        if instances != nil && len(instances.ServiceInstances) > 0 {
2✔
481
                return &instances.ServiceInstances[0], nil
1✔
482
        }
1✔
483
        log.Info("instance not found in SM")
1✔
484
        return nil, nil
1✔
485
}
486

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

1✔
490
        log.Info(fmt.Sprintf("found existing instance in SM with id %s, updating status", smInstance.ID))
1✔
491
        updateHashedSpecValue(k8sInstance)
1✔
492
        if smInstance.Ready {
2✔
493
                k8sInstance.Status.Ready = metav1.ConditionTrue
1✔
494
        }
1✔
495
        if smInstance.Shared {
1✔
496
                utils.SetSharedCondition(k8sInstance, metav1.ConditionTrue, common.ShareSucceeded, "Instance shared successfully")
×
497
        }
×
498
        k8sInstance.Status.InstanceID = smInstance.ID
1✔
499
        k8sInstance.Status.OperationURL = ""
1✔
500
        k8sInstance.Status.OperationType = ""
1✔
501
        tags, err := getOfferingTags(smClient, smInstance.ServicePlanID)
1✔
502
        if err != nil {
2✔
503
                log.Error(err, "could not recover offering tags")
1✔
504
        }
1✔
505
        if len(tags) > 0 {
1✔
506
                k8sInstance.Status.Tags = tags
×
507
        }
×
508

509
        instanceState := smClientTypes.SUCCEEDED
1✔
510
        operationType := smClientTypes.CREATE
1✔
511
        description := ""
1✔
512
        if smInstance.LastOperation != nil {
2✔
513
                instanceState = smInstance.LastOperation.State
1✔
514
                operationType = smInstance.LastOperation.Type
1✔
515
                description = smInstance.LastOperation.Description
1✔
516
        } else if !smInstance.Ready {
3✔
517
                instanceState = smClientTypes.FAILED
1✔
518
        }
1✔
519

520
        switch instanceState {
1✔
521
        case smClientTypes.PENDING:
1✔
522
                fallthrough
1✔
523
        case smClientTypes.INPROGRESS:
1✔
524
                k8sInstance.Status.OperationURL = sm.BuildOperationURL(smInstance.LastOperation.ID, smInstance.ID, smClientTypes.ServiceInstancesURL)
1✔
525
                k8sInstance.Status.OperationType = smInstance.LastOperation.Type
1✔
526
                utils.SetInProgressConditions(ctx, smInstance.LastOperation.Type, smInstance.LastOperation.Description, k8sInstance, false)
1✔
527
        case smClientTypes.SUCCEEDED:
1✔
528
                utils.SetSuccessConditions(operationType, k8sInstance, false)
1✔
529
        case smClientTypes.FAILED:
1✔
530
                utils.SetFailureConditions(operationType, description, k8sInstance, false)
1✔
531
        }
532

533
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, k8sInstance)
1✔
534
}
535

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

554
                        if err := utils.AddWatchForSecretIfNeeded(ctx, r.Client, secret, string(serviceInstance.UID)); err != nil {
1✔
555
                                log.Error(err, fmt.Sprintf("failed to mark secret for watch %s", secret.Name))
×
556
                                return nil, err
×
557
                        }
×
558

559
                }
560
        }
561

562
        //sync instance labels
563
        for labelKey, labelValue := range serviceInstance.Labels {
2✔
564
                if strings.HasPrefix(labelKey, common.InstanceSecretRefLabel) {
2✔
565
                        if _, ok := newInstanceLabels[labelKey]; !ok {
2✔
566
                                log.Info(fmt.Sprintf("params secret named %s was removed, unwatching it", labelValue))
1✔
567
                                instanceLabelsChanged = true
1✔
568
                                if err := utils.RemoveWatchForSecret(ctx, r.Client, types.NamespacedName{Name: labelValue, Namespace: serviceInstance.Namespace}, string(serviceInstance.UID)); err != nil {
1✔
569
                                        log.Error(err, fmt.Sprintf("failed to unwatch secret %s", labelValue))
×
570
                                        return nil, err
×
571
                                }
×
572
                        }
573
                } else {
×
574
                        // this label not related to secrets, add it
×
575
                        newInstanceLabels[labelKey] = labelValue
×
576
                }
×
577
        }
578
        if instanceLabelsChanged {
2✔
579
                serviceInstance.Labels = newInstanceLabels
1✔
580
                log.Info("updating instance with secret labels")
1✔
581
                return instanceParameters, r.Client.Update(ctx, serviceInstance)
1✔
582
        }
1✔
583

584
        return instanceParameters, nil
1✔
585
}
586

587
func isFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) bool {
1✔
588
        log := logutils.GetLogger(ctx)
1✔
589

1✔
590
        if serviceInstance.Status.ForceReconcile {
2✔
591
                log.Info("instance is not in final state, ForceReconcile is true")
1✔
592
                return false
1✔
593
        }
1✔
594

595
        observedGen := common.GetObservedGeneration(serviceInstance)
1✔
596
        if serviceInstance.Generation != observedGen {
2✔
597
                log.Info(fmt.Sprintf("instance is not in final state, generation: %d, observedGen: %d", serviceInstance.Generation, observedGen))
1✔
598
                return false
1✔
599
        }
1✔
600

601
        if utils.ShouldRetryOperation(serviceInstance) {
2✔
602
                log.Info("instance is not in final state, last operation failed, retrying")
1✔
603
                return false
1✔
604
        }
1✔
605

606
        if shareOrUnshareRequired(serviceInstance) {
2✔
607
                log.Info("instance is not in final state, need to sync sharing status")
1✔
608
                if len(serviceInstance.Status.HashedSpec) == 0 {
1✔
609
                        updateHashedSpecValue(serviceInstance)
×
610
                }
×
611
                return false
1✔
612
        }
613

614
        log.Info(fmt.Sprintf("instance is in final state (generation: %d)", serviceInstance.Generation))
1✔
615
        return true
1✔
616
}
617

618
func updateRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
619
        //update is not supported for failed instances (this can occur when instance creation was asynchronously)
1✔
620
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
1✔
621
                return false
×
622
        }
×
623

624
        if serviceInstance.Status.ForceReconcile {
2✔
625
                return true
1✔
626
        }
1✔
627

628
        cond := meta.FindStatusCondition(serviceInstance.Status.Conditions, common.ConditionSucceeded)
1✔
629
        if cond != nil && cond.Reason == common.UpdateInProgress { //in case of transient error occurred
1✔
630
                return true
×
631
        }
×
632

633
        return serviceInstance.GetSpecHash() != serviceInstance.Status.HashedSpec
1✔
634
}
635

636
func shareOrUnshareRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
637
        //relevant only for non-shared instances - sharing instance is possible only for usable instances
1✔
638
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
2✔
639
                return false
1✔
640
        }
1✔
641

642
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), common.ConditionShared)
1✔
643
        if sharedCondition == nil {
2✔
644
                return serviceInstance.GetShared()
1✔
645
        }
1✔
646

647
        if sharedCondition.Reason == common.ShareNotSupported {
2✔
648
                return false
1✔
649
        }
1✔
650

651
        if sharedCondition.Status == metav1.ConditionFalse {
2✔
652
                // instance does not appear to be shared, should share it if shared is requested
1✔
653
                return serviceInstance.GetShared()
1✔
654
        }
1✔
655

656
        // instance appears to be shared, should unshare it if shared is not requested
657
        return !serviceInstance.GetShared()
1✔
658
}
659

660
func getOfferingTags(smClient sm.Client, planID string) ([]string, error) {
1✔
661
        planQuery := &sm.Parameters{
1✔
662
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", planID)},
1✔
663
        }
1✔
664
        plans, err := smClient.ListPlans(planQuery)
1✔
665
        if err != nil {
1✔
666
                return nil, err
×
667
        }
×
668

669
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
670
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
671
        }
1✔
672

673
        offeringQuery := &sm.Parameters{
×
674
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
675
        }
×
676

×
677
        offerings, err := smClient.ListOfferings(offeringQuery)
×
678
        if err != nil {
×
679
                return nil, err
×
680
        }
×
681
        if offerings == nil || len(offerings.ServiceOfferings) != 1 {
×
682
                return nil, fmt.Errorf("could not find offering with id %s", plans.ServicePlans[0].ServiceOfferingID)
×
683
        }
×
684

685
        var tags []string
×
686
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
687
                return nil, err
×
688
        }
×
689
        return tags, nil
×
690
}
691

692
func getTags(tags []byte) ([]string, error) {
1✔
693
        var tagsArr []string
1✔
694
        if err := json.Unmarshal(tags, &tagsArr); err != nil {
1✔
695
                return nil, err
×
696
        }
×
697
        return tagsArr, nil
1✔
698
}
699

700
func updateHashedSpecValue(serviceInstance *v1.ServiceInstance) {
1✔
701
        serviceInstance.Status.HashedSpec = serviceInstance.GetSpecHash()
1✔
702
}
1✔
703

704
func getErrorMsgFromLastOperation(status *smClientTypes.Operation) string {
1✔
705
        errMsg := "async operation error"
1✔
706
        if status == nil || len(status.Errors) == 0 {
1✔
707
                return errMsg
×
708
        }
×
709
        var errMap map[string]interface{}
1✔
710

1✔
711
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
712
                return errMsg
×
713
        }
×
714

715
        if description, found := errMap["description"]; found {
2✔
716
                if descStr, ok := description.(string); ok {
2✔
717
                        errMsg = descStr
1✔
718
                }
1✔
719
        }
720
        return errMsg
1✔
721
}
722

723
type SecretPredicate struct {
724
        predicate.Funcs
725
}
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