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

SAP / sap-btp-service-operator / 20819494810

08 Jan 2026 02:04PM UTC coverage: 78.346% (-0.09%) from 78.436%
20819494810

Pull #593

github

kerenlahav
Merge remote-tracking branch 'origin/move' into move
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
        smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
91
        if err != nil {
1✔
NEW
92
                log.Error(err, "failed to get sm client")
×
NEW
93
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, common.Unknown, err)
×
NEW
94
        }
×
95

96
        if utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) {
2✔
97
                return r.deleteInstance(ctx, serviceInstance)
1✔
98
        }
1✔
99

100
        // If stored hash is MD5 (32 chars) and we're now using SHA256 (64 chars),
101
        // perform one-time migration by updating the stored hash without triggering update
102
        if len(serviceInstance.Status.HashedSpec) == 32 {
2✔
103
                // This is likely an MD5->SHA256 migration, update the stored hash silently
1✔
104
                // to prevent unnecessary service updates during FIPS migration
1✔
105
                log.Info(fmt.Sprintf("updated hashing for instance '%s' (id=%s)", serviceInstance.Name, serviceInstance.Status.InstanceID))
1✔
106
                updateHashedSpecValue(serviceInstance)
1✔
107
                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
108
        }
1✔
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
                        }
NEW
129
                        log.Error(err, fmt.Sprintf("failed to get instance %s from SM", serviceInstance.Status.InstanceID))
×
NEW
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✔
NEW
137
                        return ctrl.Result{}, err
×
NEW
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
                if len(serviceInstance.Status.HashedSpec) == 0 {
2✔
148
                        updateHashedSpecValue(serviceInstance)
1✔
149
                        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
150
                }
1✔
151

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

572
                }
573
        }
574

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

597
        return instanceParameters, nil
1✔
598
}
599

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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