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

SAP / sap-btp-service-operator / 25311451957

04 May 2026 09:27AM UTC coverage: 78.143%. First build
25311451957

Pull #631

github

kerenlahav
tests
Pull Request #631: async provision retry fix

47 of 65 new or added lines in 5 files covered. (72.31%)

2828 of 3619 relevant lines covered (78.14%)

0.88 hits per line

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

81.44
/controllers/serviceinstance_controller.go
1
/*
2

3

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

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

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

17
package controllers
18

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

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

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

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

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

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

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

49
        "github.com/google/uuid"
50

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

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

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

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

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

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

97
        if len(serviceInstance.Status.OperationURL) > 0 &&
1✔
98
                (serviceInstance.Status.OperationType == smClientTypes.DELETE || !utils.IsMarkedForDeletion(serviceInstance.ObjectMeta)) {
2✔
99
                // ongoing operation - poll status from SM
1✔
100
                return r.poll(ctx, serviceInstance, smClient)
1✔
101
        }
1✔
102

103
        if shouldInstanceBeDeleted(serviceInstance) {
2✔
104
                return r.deleteInstance(ctx, serviceInstance, smClient)
1✔
105
        }
1✔
106

107
        // If stored hash is MD5 (32 chars) and we're now using SHA256 (64 chars),
108
        // perform one-time migration by updating the stored hash without triggering update
109
        if len(serviceInstance.Status.HashedSpec) == 32 {
2✔
110
                // This is likely an MD5->SHA256 migration, update the stored hash silently
1✔
111
                // to prevent unnecessary service updates during FIPS migration
1✔
112
                log.Info(fmt.Sprintf("updated hashing for instance '%s' (id=%s)", serviceInstance.Name, serviceInstance.Status.InstanceID))
1✔
113
                updateHashedSpecValue(serviceInstance)
1✔
114
                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
115
        }
1✔
116

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

141
        if len(serviceInstance.GetConditions()) == 0 {
2✔
142
                err := utils.InitConditions(ctx, r.Client, serviceInstance)
1✔
143
                if err != nil {
1✔
144
                        return ctrl.Result{}, err
×
145
                }
×
146
        }
147

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

288
func (r *ServiceInstanceReconciler) deleteInstance(ctx context.Context, serviceInstance *v1.ServiceInstance, smClient sm.Client) (ctrl.Result, error) {
1✔
289
        log := logutils.GetLogger(ctx)
1✔
290

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

309
                log.Info(fmt.Sprintf("Deleting instance with id %v from SM", serviceInstance.Status.InstanceID))
1✔
310
                operationURL, deprovisionErr := smClient.Deprovision(serviceInstance.Status.InstanceID, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
311
                if deprovisionErr != nil {
2✔
312
                        return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.DELETE, deprovisionErr)
1✔
313
                }
1✔
314

315
                if operationURL != "" {
2✔
316
                        log.Info("Deleting instance async")
1✔
317
                        return r.handleAsyncDelete(ctx, serviceInstance, operationURL)
1✔
318
                }
1✔
319

320
                for key, secretName := range serviceInstance.Labels {
2✔
321
                        if strings.HasPrefix(key, common.InstanceSecretRefLabel) {
2✔
322
                                if err := utils.RemoveWatchForSecret(ctx, r.Client, types.NamespacedName{Name: secretName, Namespace: serviceInstance.Namespace}, string(serviceInstance.UID)); err != nil {
1✔
323
                                        log.Error(err, fmt.Sprintf("failed to unwatch secret %s", secretName))
×
324
                                        return ctrl.Result{}, err
×
325
                                }
×
326
                        }
327
                }
328

329
                serviceInstance.Status.InstanceID = ""
1✔
330
                serviceInstance.Status.AsyncProvisionFailed = nil
1✔
331
                if err := r.Client.Status().Update(ctx, serviceInstance); err != nil {
1✔
332
                        log.Error(err, "failed to update service instance status after deletion")
×
333
                        return ctrl.Result{}, err
×
334
                }
×
335
                log.Info("Instance was deleted successfully, removing finalizer")
1✔
336
                // remove our finalizer from the list and update it.
1✔
337
                return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName)
1✔
338
        }
339
        return ctrl.Result{}, nil
1✔
340
}
341

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

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

373
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
374
}
375

376
func (r *ServiceInstanceReconciler) poll(ctx context.Context, serviceInstance *v1.ServiceInstance, smClient sm.Client) (ctrl.Result, error) {
1✔
377
        log := logutils.GetLogger(ctx)
1✔
378
        log.Info(fmt.Sprintf("instance resource is in progress, found operation url %s for operation type %s", serviceInstance.Status.OperationURL, serviceInstance.Status.OperationType))
1✔
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
                log.Info(fmt.Sprintf("operation %s %s is still in progress", serviceInstance.Status.OperationType, serviceInstance.Status.OperationURL))
1✔
404
                if len(status.Description) > 0 {
1✔
405
                        log.Info(fmt.Sprintf("last operation description is '%s'", status.Description))
×
406
                        utils.SetInProgressConditions(ctx, status.Type, status.Description, serviceInstance, true)
×
407
                        if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
×
408
                                log.Error(err, "unable to update ServiceInstance polling description")
×
409
                                return ctrl.Result{}, err
×
410
                        }
×
411
                }
412
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
413
        case smClientTypes.FAILED:
1✔
414
                errMsg := getErrorMsgFromLastOperation(status)
1✔
415
                log.Info(fmt.Sprintf("operation %s %s failed, error: %s", serviceInstance.Status.OperationType, serviceInstance.Status.OperationURL, errMsg))
1✔
416
                utils.SetFailureConditions(status.Type, errMsg, serviceInstance, true)
1✔
417
                if serviceInstance.Status.OperationType == smClientTypes.CREATE {
2✔
418
                        log.Info(fmt.Sprintf("async provision failed for instance %s", serviceInstance.Status.InstanceID))
1✔
419
                        trueVal := true
1✔
420
                        serviceInstance.Status.AsyncProvisionFailed = &trueVal
1✔
421
                } else if serviceInstance.Status.OperationType == smClientTypes.DELETE {
3✔
422
                        serviceInstance.Status.AsyncProvisionFailed = nil
1✔
423
                }
1✔
424
                serviceInstance.Status.OperationURL = ""
1✔
425
                serviceInstance.Status.OperationType = ""
1✔
426
                if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
1✔
427
                        return ctrl.Result{}, err
×
428
                }
×
429
                return ctrl.Result{}, errors.New(errMsg)
1✔
430
        case smClientTypes.SUCCEEDED:
1✔
431
                log.Info(fmt.Sprintf("operation %s %s completed succefully", serviceInstance.Status.OperationType, serviceInstance.Status.OperationURL))
1✔
432
                if serviceInstance.Status.OperationType == smClientTypes.CREATE {
2✔
433
                        smInstance, err := smClient.GetInstanceByID(serviceInstance.Status.InstanceID, nil)
1✔
434
                        if err != nil {
1✔
435
                                log.Error(err, fmt.Sprintf("instance %s succeeded but could not fetch it from SM", serviceInstance.Status.InstanceID))
×
436
                                return ctrl.Result{}, err
×
437
                        }
×
438
                        if len(smInstance.Labels["subaccount_id"]) > 0 {
2✔
439
                                serviceInstance.Status.SubaccountID = smInstance.Labels["subaccount_id"][0]
1✔
440
                        }
1✔
441
                        serviceInstance.Status.Ready = metav1.ConditionTrue
1✔
442
                } else if serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
443
                        log.Info(fmt.Sprintf("instance %s deleted successfully from sm, removing finalizer", serviceInstance.Status.InstanceID))
1✔
444
                        if err := utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName); err != nil {
1✔
445
                                return ctrl.Result{}, err
×
446
                        }
×
447
                        serviceInstance.Status.OperationURL = ""
1✔
448
                        serviceInstance.Status.OperationType = ""
1✔
449
                        serviceInstance.Status.InstanceID = ""
1✔
450
                        serviceInstance.Status.AsyncProvisionFailed = nil
1✔
451
                        return ctrl.Result{RequeueAfter: time.Second}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
452
                }
453
                utils.SetSuccessConditions(status.Type, serviceInstance, true)
1✔
454
        }
455

456
        serviceInstance.Status.OperationURL = ""
1✔
457
        serviceInstance.Status.OperationType = ""
1✔
458
        serviceInstance.Status.AsyncProvisionFailed = nil
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
        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
469
}
1✔
470

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

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

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

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

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

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

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

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

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

568
                        if err := utils.AddWatchForSecretIfNeeded(ctx, r.Client, secret, string(serviceInstance.UID)); err != nil {
1✔
569
                                log.Error(err, fmt.Sprintf("failed to mark secret for watch %s", secret.Name))
×
570
                                return nil, err
×
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 (r *ServiceInstanceReconciler) maintainFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
601
        log := logutils.GetLogger(ctx)
1✔
602

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

763
func shouldInstanceBeDeleted(serviceInstance *v1.ServiceInstance) bool {
1✔
764
        return utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) ||
1✔
765
                serviceInstance.IsAsyncProvisionFailed()
1✔
766
}
1✔
767

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