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

SAP / sap-btp-service-operator / 26077724891

19 May 2026 05:14AM UTC coverage: 77.939%. First build
26077724891

Pull #635

github

web-flow
Merge branch 'main' into fixasync2
Pull Request #635: Async retry with backoff

114 of 148 new or added lines in 4 files covered. (77.03%)

2897 of 3717 relevant lines covered (77.94%)

0.88 hits per line

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

81.31
/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
        Retries     *utils.RetryStore
69
}
70

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

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

1✔
80
        retry := r.Retries.Get(req.NamespacedName)
1✔
81
        if retry != nil && time.Now().Before(retry.NextRetry) {
2✔
82
                remaining := time.Until(retry.NextRetry)
1✔
83
                log.Info(fmt.Sprintf("skipping instance reconcile due to backoff. attempts=%d retryIn=%s", retry.Attempts, remaining))
1✔
84

1✔
85
                return ctrl.Result{RequeueAfter: remaining}, nil
1✔
86
        }
1✔
87

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

1✔
100
        log.Info(fmt.Sprintf("*** staring reconcile of ServiceInstance %s/%s ***", serviceInstance.Namespace, serviceInstance.Name))
1✔
101
        smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
102
        if err != nil {
1✔
103
                log.Error(err, "failed to get sm client")
×
104
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, common.Unknown, err)
×
105
        }
×
106
        if len(serviceInstance.Status.OperationURL) > 0 &&
1✔
107
                (serviceInstance.Status.OperationType == smClientTypes.DELETE || !utils.IsMarkedForDeletion(serviceInstance.ObjectMeta)) {
2✔
108
                // ongoing operation - poll status from SM
1✔
109
                return r.poll(ctx, smClient, serviceInstance)
1✔
110
        }
1✔
111

112
        if utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) {
2✔
113
                return r.deleteInstance(ctx, smClient, serviceInstance)
1✔
114
        }
1✔
115

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

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

150
        if len(serviceInstance.GetConditions()) == 0 {
2✔
151
                err := utils.InitConditions(ctx, r.Client, serviceInstance)
1✔
152
                if err != nil {
1✔
153
                        return ctrl.Result{}, err
×
154
                }
×
155
        }
156

157
        if isFinalState(ctx, serviceInstance) {
2✔
158
                return r.maintainFinalState(ctx, serviceInstance)
1✔
159
        }
1✔
160

161
        if controllerutil.AddFinalizer(serviceInstance, common.FinalizerName) {
2✔
162
                log.Info(fmt.Sprintf("added finalizer '%s' to service instance", common.FinalizerName))
1✔
163
                if err := r.Client.Update(ctx, serviceInstance); err != nil {
1✔
164
                        return ctrl.Result{}, err
×
165
                }
×
166
        }
167

168
        if serviceInstance.Status.InstanceID == "" {
2✔
169
                log.Info("Instance ID is empty, checking if instance exist in SM")
1✔
170
                smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
171
                if err != nil {
1✔
172
                        log.Error(err, "failed to check instance recovery")
×
NEW
173
                        return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.CREATE, err, true)
×
174
                }
×
175
                if smInstance != nil {
2✔
176
                        return r.recover(ctx, smClient, serviceInstance, smInstance)
1✔
177
                }
1✔
178

179
                // if instance was not recovered then create new instance
180
                return r.createInstance(ctx, smClient, serviceInstance)
1✔
181
        }
182

183
        if updateRequired(serviceInstance) {
2✔
184
                return r.updateInstance(ctx, smClient, serviceInstance)
1✔
185
        }
1✔
186

187
        // share/unshare
188
        if shareOrUnshareRequired(serviceInstance) {
2✔
189
                return r.handleInstanceSharing(ctx, serviceInstance, smClient)
1✔
190
        }
1✔
191

192
        log.Info("No action required")
1✔
193
        return ctrl.Result{}, nil
1✔
194
}
195

196
func (r *ServiceInstanceReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
197
        return ctrl.NewControllerManagedBy(mgr).
1✔
198
                For(&v1.ServiceInstance{}).
1✔
199
                WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](r.Config.RetryBaseDelay, r.Config.RetryMaxDelay)}).
1✔
200
                Complete(r)
1✔
201
}
1✔
202

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

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

1✔
225
        if provisionErr != nil {
2✔
226
                log.Error(provisionErr, "failed to create service instance", "serviceOfferingName", serviceInstance.Spec.ServiceOfferingName,
1✔
227
                        "servicePlanName", serviceInstance.Spec.ServicePlanName)
1✔
228
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.CREATE, provisionErr, true)
1✔
229
        }
1✔
230

231
        serviceInstance.Status.InstanceID = provision.InstanceID
1✔
232
        serviceInstance.Status.SubaccountID = provision.SubaccountID
1✔
233
        if len(provision.Tags) > 0 {
2✔
234
                tags, err := getTags(provision.Tags)
1✔
235
                if err != nil {
1✔
236
                        log.Error(err, "failed to unmarshal tags")
×
237
                } else {
1✔
238
                        serviceInstance.Status.Tags = tags
1✔
239
                }
1✔
240
        }
241

242
        if provision.Location != "" {
2✔
243
                log.Info("Provision request is in progress (async)")
1✔
244
                serviceInstance.Status.OperationURL = provision.Location
1✔
245
                serviceInstance.Status.OperationType = smClientTypes.CREATE
1✔
246
                utils.SetInProgressConditions(ctx, smClientTypes.CREATE, "", serviceInstance, false)
1✔
247

1✔
248
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
249
        }
1✔
250

251
        log.Info(fmt.Sprintf("Instance provisioned successfully, instanceID: %s, subaccountID: %s", serviceInstance.Status.InstanceID,
1✔
252
                serviceInstance.Status.SubaccountID))
1✔
253
        r.Retries.Reset(types.NamespacedName{Name: serviceInstance.Name, Namespace: serviceInstance.Namespace})
1✔
254
        utils.SetSuccessConditions(smClientTypes.CREATE, serviceInstance, false)
1✔
255
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
256
}
257

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

1✔
262
        instanceParameters, err := r.buildSMRequestParameters(ctx, serviceInstance)
1✔
263
        if err != nil {
2✔
264
                log.Error(err, "failed to parse instance parameters")
1✔
265
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, smClientTypes.UPDATE, err)
1✔
266
        }
1✔
267

268
        updateHashedSpecValue(serviceInstance)
1✔
269
        _, operationURL, err := smClient.UpdateInstance(serviceInstance.Status.InstanceID, &smClientTypes.ServiceInstance{
1✔
270
                Name:          serviceInstance.Spec.ExternalName,
1✔
271
                ServicePlanID: serviceInstance.Spec.ServicePlanID,
1✔
272
                Parameters:    instanceParameters,
1✔
273
        }, serviceInstance.Spec.ServiceOfferingName, serviceInstance.Spec.ServicePlanName, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo), serviceInstance.Spec.DataCenter)
1✔
274

1✔
275
        if err != nil {
2✔
276
                log.Error(err, fmt.Sprintf("failed to update service instance with ID %s", serviceInstance.Status.InstanceID))
1✔
277
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.UPDATE, err, true)
1✔
278
        }
1✔
279

280
        if operationURL != "" {
2✔
281
                log.Info(fmt.Sprintf("Update request accepted, operation URL: %s", operationURL))
1✔
282
                serviceInstance.Status.OperationURL = operationURL
1✔
283
                serviceInstance.Status.OperationType = smClientTypes.UPDATE
1✔
284
                utils.SetInProgressConditions(ctx, smClientTypes.UPDATE, "", serviceInstance, false)
1✔
285
                serviceInstance.Status.ForceReconcile = false
1✔
286
                if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
2✔
287
                        return ctrl.Result{}, err
1✔
288
                }
1✔
289

290
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
291
        }
292
        log.Info("Instance updated successfully")
1✔
293
        utils.SetSuccessConditions(smClientTypes.UPDATE, serviceInstance, false)
1✔
294
        serviceInstance.Status.ForceReconcile = false
1✔
295
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
296
}
297

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

1✔
301
        if controllerutil.ContainsFinalizer(serviceInstance, common.FinalizerName) {
2✔
302
                log.Info("instance has finalizer, deleting it from sm")
1✔
303
                if len(serviceInstance.Status.InstanceID) == 0 {
2✔
304
                        log.Info("No instance id found validating instance does not exists in SM before removing finalizer")
1✔
305
                        smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
306
                        if err != nil {
1✔
NEW
307
                                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.DELETE, err, true)
×
308
                        }
×
309
                        if smInstance != nil {
2✔
310
                                log.Info("instance exists in SM continue with deletion")
1✔
311
                                serviceInstance.Status.InstanceID = smInstance.ID
1✔
312
                                utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "delete after recovery", serviceInstance, false)
1✔
313
                                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
314
                        }
1✔
315
                        log.Info("instance does not exists in SM, removing finalizer")
1✔
316
                        return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName)
1✔
317
                }
318

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

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

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

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

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

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

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

385
func (r *ServiceInstanceReconciler) poll(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
386
        log := logutils.GetLogger(ctx)
1✔
387
        log.Info(fmt.Sprintf("instance %s is '%s' in progress, polling operation %s", serviceInstance.Status.InstanceID, serviceInstance.Status.OperationType, serviceInstance.Status.OperationURL))
1✔
388
        status, statusErr := smClient.Status(serviceInstance.Status.OperationURL, nil)
1✔
389
        if statusErr != nil {
1✔
390
                log.Info(fmt.Sprintf("failed to fetch operation, got error from SM: %s", statusErr.Error()), "operationURL", serviceInstance.Status.OperationURL)
×
391
                utils.SetInProgressConditions(ctx, serviceInstance.Status.OperationType, string(smClientTypes.INPROGRESS), serviceInstance, false)
×
392
                // if failed to read operation status we cleanup the status to trigger re-sync from SM
×
393
                freshStatus := v1.ServiceInstanceStatus{Conditions: serviceInstance.GetConditions()}
×
394
                if utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) {
×
395
                        freshStatus.InstanceID = serviceInstance.Status.InstanceID
×
396
                }
×
397
                serviceInstance.Status = freshStatus
×
398
                if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
×
399
                        log.Error(err, "failed to update status during polling")
×
400
                }
×
401
                return ctrl.Result{}, statusErr
×
402
        }
403

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

465
        serviceInstance.Status.OperationURL = ""
1✔
466
        serviceInstance.Status.OperationType = ""
1✔
467
        r.Retries.Reset(types.NamespacedName{Name: serviceInstance.Name, Namespace: serviceInstance.Namespace})
1✔
468

1✔
469
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
470
}
471

472
func (r *ServiceInstanceReconciler) handleFailedAsyncProvision(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
473
        log := logutils.GetLogger(ctx)
1✔
474
        log.Info(fmt.Sprintf("handleFailedAsyncProvision deleting instance that failed to be provisioned with id %s from SM", serviceInstance.Status.InstanceID))
1✔
475
        operationURL, deprovisionErr := smClient.Deprovision(serviceInstance.Status.InstanceID, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
476
        if deprovisionErr != nil {
1✔
NEW
477
                log.Error(deprovisionErr, fmt.Sprintf("handleFailedAsyncProvision failed to deprovision instance: %s", serviceInstance.Status.InstanceID))
×
NEW
478
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.DELETE, deprovisionErr, false)
×
NEW
479
        }
×
480

481
        if operationURL != "" {
1✔
NEW
482
                log.Info(fmt.Sprintf("handleFailedAsyncProvision deletion is async, operation url %s", operationURL))
×
NEW
483
                serviceInstance.Status.OperationURL = operationURL
×
NEW
484
                serviceInstance.Status.OperationType = smClientTypes.DELETE
×
NEW
485
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
×
NEW
486
        }
×
487

488
        log.Info(fmt.Sprintf("handleFailedAsyncProvision instance %s deleted successfully", serviceInstance.Status.InstanceID))
1✔
489
        serviceInstance.Status.OperationURL = ""
1✔
490
        serviceInstance.Status.OperationType = ""
1✔
491
        serviceInstance.Status.InstanceID = ""
1✔
492
        if err := r.Client.Status().Update(ctx, serviceInstance); err != nil {
1✔
NEW
493
                log.Error(err, "handleFailedAsyncProvision failed to update service instance status after deletion")
×
NEW
494
                return ctrl.Result{}, err
×
NEW
495
        }
×
496
        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
497
}
498

499
func (r *ServiceInstanceReconciler) handleAsyncDelete(ctx context.Context, serviceInstance *v1.ServiceInstance, opURL string) (ctrl.Result, error) {
1✔
500
        serviceInstance.Status.OperationURL = opURL
1✔
501
        serviceInstance.Status.OperationType = smClientTypes.DELETE
1✔
502
        utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "", serviceInstance, false)
1✔
503

1✔
504
        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
505
}
1✔
506

507
func (r *ServiceInstanceReconciler) getInstanceForRecovery(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (*smClientTypes.ServiceInstance, error) {
1✔
508
        log := logutils.GetLogger(ctx)
1✔
509
        parameters := sm.Parameters{
1✔
510
                FieldQuery: []string{
1✔
511
                        fmt.Sprintf("name eq '%s'", serviceInstance.Spec.ExternalName),
1✔
512
                        fmt.Sprintf("context/clusterid eq '%s'", r.Config.ClusterID),
1✔
513
                        fmt.Sprintf("context/namespace eq '%s'", serviceInstance.Namespace)},
1✔
514
                LabelQuery: []string{
1✔
515
                        fmt.Sprintf("%s eq '%s'", common.K8sNameLabel, serviceInstance.Name)},
1✔
516
                GeneralParams: []string{"attach_last_operations=true"},
1✔
517
        }
1✔
518

1✔
519
        instances, err := smClient.ListInstances(&parameters)
1✔
520
        if err != nil {
1✔
521
                log.Error(err, "failed to list instances in SM")
×
522
                return nil, err
×
523
        }
×
524

525
        if instances != nil && len(instances.ServiceInstances) > 0 {
2✔
526
                return &instances.ServiceInstances[0], nil
1✔
527
        }
1✔
528
        log.Info("instance not found in SM")
1✔
529
        return nil, nil
1✔
530
}
531

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

1✔
535
        log.Info(fmt.Sprintf("found existing instance in SM with id %s, updating status", smInstance.ID))
1✔
536
        updateHashedSpecValue(k8sInstance)
1✔
537
        if smInstance.Ready {
2✔
538
                k8sInstance.Status.Ready = metav1.ConditionTrue
1✔
539
        }
1✔
540
        if smInstance.Shared {
1✔
541
                utils.SetSharedCondition(k8sInstance, metav1.ConditionTrue, common.ShareSucceeded, "Instance shared successfully")
×
542
        }
×
543
        k8sInstance.Status.InstanceID = smInstance.ID
1✔
544
        k8sInstance.Status.OperationURL = ""
1✔
545
        k8sInstance.Status.OperationType = ""
1✔
546
        tags, err := getOfferingTags(smClient, smInstance.ServicePlanID)
1✔
547
        if err != nil {
2✔
548
                log.Error(err, "could not recover offering tags")
1✔
549
        }
1✔
550
        if len(tags) > 0 {
1✔
551
                k8sInstance.Status.Tags = tags
×
552
        }
×
553

554
        instanceState := smClientTypes.SUCCEEDED
1✔
555
        operationType := smClientTypes.CREATE
1✔
556
        description := ""
1✔
557
        if smInstance.LastOperation != nil {
2✔
558
                instanceState = smInstance.LastOperation.State
1✔
559
                operationType = smInstance.LastOperation.Type
1✔
560
                description = smInstance.LastOperation.Description
1✔
561
        } else if !smInstance.Ready {
3✔
562
                instanceState = smClientTypes.FAILED
1✔
563
        }
1✔
564

565
        switch instanceState {
1✔
566
        case smClientTypes.PENDING:
1✔
567
                fallthrough
1✔
568
        case smClientTypes.INPROGRESS:
1✔
569
                k8sInstance.Status.OperationURL = sm.BuildOperationURL(smInstance.LastOperation.ID, smInstance.ID, smClientTypes.ServiceInstancesURL)
1✔
570
                k8sInstance.Status.OperationType = smInstance.LastOperation.Type
1✔
571
                k8sInstance.Status.InstanceID = smInstance.ID
1✔
572
                utils.SetInProgressConditions(ctx, smInstance.LastOperation.Type, smInstance.LastOperation.Description, k8sInstance, false)
1✔
573
        case smClientTypes.SUCCEEDED:
1✔
574
                utils.SetSuccessConditions(operationType, k8sInstance, false)
1✔
575
        case smClientTypes.FAILED:
1✔
576
                utils.SetFailureConditions(operationType, description, k8sInstance, false)
1✔
577
                //if operationType == smClientTypes.CREATE {
578
                //        trueVal := true
579
                //        k8sInstance.Status.AsyncProvisionFailed = &trueVal
580
                //}
581
        }
582

583
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, k8sInstance)
1✔
584
}
585

586
func (r *ServiceInstanceReconciler) buildSMRequestParameters(ctx context.Context, serviceInstance *v1.ServiceInstance) ([]byte, error) {
1✔
587
        log := logutils.GetLogger(ctx)
1✔
588
        instanceParameters, paramSecrets, err := utils.BuildSMRequestParameters(serviceInstance.Namespace, serviceInstance.Spec.Parameters, serviceInstance.Spec.ParametersFrom)
1✔
589
        if err != nil {
2✔
590
                log.Error(err, "failed to build instance parameters")
1✔
591
                return nil, err
1✔
592
        }
1✔
593
        instanceLabelsChanged := false
1✔
594
        newInstanceLabels := make(map[string]string)
1✔
595
        if serviceInstance.IsSubscribedToParamSecretsChanges() {
2✔
596
                // find all new secrets on the instance
1✔
597
                for _, secret := range paramSecrets {
2✔
598
                        labelKey := utils.GetLabelKeyForInstanceSecret(secret.Name)
1✔
599
                        newInstanceLabels[labelKey] = secret.Name
1✔
600
                        if _, ok := serviceInstance.Labels[labelKey]; !ok {
2✔
601
                                instanceLabelsChanged = true
1✔
602
                        }
1✔
603

604
                        if err := utils.AddWatchForSecretIfNeeded(ctx, r.Client, secret, string(serviceInstance.UID)); err != nil {
1✔
605
                                log.Error(err, fmt.Sprintf("failed to mark secret for watch %s", secret.Name))
×
606
                                return nil, err
×
607
                        }
×
608
                }
609
        }
610

611
        //sync instance labels
612
        for labelKey, labelValue := range serviceInstance.Labels {
2✔
613
                if strings.HasPrefix(labelKey, common.InstanceSecretRefLabel) {
2✔
614
                        if _, ok := newInstanceLabels[labelKey]; !ok {
2✔
615
                                log.Info(fmt.Sprintf("params secret named %s was removed, unwatching it", labelValue))
1✔
616
                                instanceLabelsChanged = true
1✔
617
                                if err := utils.RemoveWatchForSecret(ctx, r.Client, types.NamespacedName{Name: labelValue, Namespace: serviceInstance.Namespace}, string(serviceInstance.UID)); err != nil {
1✔
618
                                        log.Error(err, fmt.Sprintf("failed to unwatch secret %s", labelValue))
×
619
                                        return nil, err
×
620
                                }
×
621
                        }
622
                } else {
×
623
                        // this label not related to secrets, add it
×
624
                        newInstanceLabels[labelKey] = labelValue
×
625
                }
×
626
        }
627
        if instanceLabelsChanged {
2✔
628
                serviceInstance.Labels = newInstanceLabels
1✔
629
                log.Info("updating instance with secret labels")
1✔
630
                return instanceParameters, r.Client.Update(ctx, serviceInstance)
1✔
631
        }
1✔
632

633
        return instanceParameters, nil
1✔
634
}
635

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

1✔
639
        if serviceInstance.IsSubscribedToParamSecretsChanges() {
2✔
640
                log.Info("instance is in final state, WatchParametersFromChanges is true, validating that all parameters secrets are watched")
1✔
641
                for _, param := range serviceInstance.Spec.ParametersFrom {
2✔
642
                        if param.SecretKeyRef != nil {
2✔
643
                                secret := &corev1.Secret{}
1✔
644
                                if err := utils.GetSecretWithFallback(ctx, types.NamespacedName{Name: param.SecretKeyRef.Name, Namespace: serviceInstance.Namespace}, secret); err != nil {
1✔
645
                                        log.Error(err, fmt.Sprintf("failed to get secret %s", param.SecretKeyRef.Name))
×
646
                                        return ctrl.Result{}, err
×
647
                                }
×
648
                                if err := utils.AddWatchForSecretIfNeeded(ctx, r.Client, secret, string(serviceInstance.UID)); err != nil {
1✔
649
                                        log.Error(err, fmt.Sprintf("failed to mark secret for watch %s", param.SecretKeyRef.Name))
×
650
                                        return ctrl.Result{}, err
×
651
                                }
×
652
                        }
653
                }
654
        }
655

656
        if len(serviceInstance.Status.HashedSpec) == 0 {
2✔
657
                log.Info("instance is missing HashedSpec value, updating it")
1✔
658
                updateHashedSpecValue(serviceInstance)
1✔
659
                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
660
        }
1✔
661
        return ctrl.Result{}, nil
1✔
662
}
663

664
func isFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) bool {
1✔
665
        log := logutils.GetLogger(ctx)
1✔
666

1✔
667
        if !serviceInstanceReady(serviceInstance) {
2✔
668
                return false
1✔
669
        }
1✔
670

671
        if serviceInstance.Status.ForceReconcile {
2✔
672
                log.Info("instance is not in final state, ForceReconcile is true")
1✔
673
                return false
1✔
674
        }
1✔
675

676
        observedGen := common.GetObservedGeneration(serviceInstance)
1✔
677
        if serviceInstance.Generation != observedGen {
2✔
678
                log.Info(fmt.Sprintf("instance is not in final state, generation: %d, observedGen: %d", serviceInstance.Generation, observedGen))
1✔
679
                return false
1✔
680
        }
1✔
681

682
        if shareOrUnshareRequired(serviceInstance) {
2✔
683
                log.Info("instance is not in final state, need to sync sharing status")
1✔
684
                if len(serviceInstance.Status.HashedSpec) == 0 {
1✔
685
                        updateHashedSpecValue(serviceInstance)
×
686
                }
×
687
                return false
1✔
688
        }
689

690
        log.Info(fmt.Sprintf("instance is in final state (generation: %d)", serviceInstance.Generation))
1✔
691
        return true
1✔
692
}
693

694
func updateRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
695
        //update is not supported for failed instances (this can occur when instance creation was asynchronously)
1✔
696
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
2✔
697
                return false
1✔
698
        }
1✔
699

700
        if serviceInstance.Status.ForceReconcile {
2✔
701
                return true
1✔
702
        }
1✔
703

704
        cond := meta.FindStatusCondition(serviceInstance.Status.Conditions, common.ConditionSucceeded)
1✔
705
        if cond != nil && cond.Reason == common.UpdateInProgress { //in case of transient error occurred
1✔
706
                return true
×
707
        }
×
708

709
        return serviceInstance.GetSpecHash() != serviceInstance.Status.HashedSpec
1✔
710
}
711

712
func shareOrUnshareRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
713
        //relevant only for non-shared instances - sharing instance is possible only for usable instances
1✔
714
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
2✔
715
                return false
1✔
716
        }
1✔
717

718
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), common.ConditionShared)
1✔
719
        if sharedCondition == nil {
2✔
720
                return serviceInstance.GetShared()
1✔
721
        }
1✔
722

723
        if sharedCondition.Reason == common.ShareNotSupported {
2✔
724
                return false
1✔
725
        }
1✔
726

727
        if sharedCondition.Status == metav1.ConditionFalse {
2✔
728
                // instance does not appear to be shared, should share it if shared is requested
1✔
729
                return serviceInstance.GetShared()
1✔
730
        }
1✔
731

732
        // instance appears to be shared, should unshare it if shared is not requested
733
        return !serviceInstance.GetShared()
1✔
734
}
735

736
func getOfferingTags(smClient sm.Client, planID string) ([]string, error) {
1✔
737
        planQuery := &sm.Parameters{
1✔
738
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", planID)},
1✔
739
        }
1✔
740
        plans, err := smClient.ListPlans(planQuery)
1✔
741
        if err != nil {
1✔
742
                return nil, err
×
743
        }
×
744

745
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
746
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
747
        }
1✔
748

749
        offeringQuery := &sm.Parameters{
×
750
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
751
        }
×
752

×
753
        offerings, err := smClient.ListOfferings(offeringQuery)
×
754
        if err != nil {
×
755
                return nil, err
×
756
        }
×
757
        if offerings == nil || len(offerings.ServiceOfferings) != 1 {
×
758
                return nil, fmt.Errorf("could not find offering with id %s", plans.ServicePlans[0].ServiceOfferingID)
×
759
        }
×
760

761
        var tags []string
×
762
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
763
                return nil, err
×
764
        }
×
765
        return tags, nil
×
766
}
767

768
func getTags(tags []byte) ([]string, error) {
1✔
769
        var tagsArr []string
1✔
770
        if err := json.Unmarshal(tags, &tagsArr); err != nil {
1✔
771
                return nil, err
×
772
        }
×
773
        return tagsArr, nil
1✔
774
}
775

776
func updateHashedSpecValue(serviceInstance *v1.ServiceInstance) {
1✔
777
        serviceInstance.Status.HashedSpec = serviceInstance.GetSpecHash()
1✔
778
}
1✔
779

780
func getErrorMsgFromLastOperation(status *smClientTypes.Operation) string {
1✔
781
        errMsg := "async operation error"
1✔
782
        if status == nil || len(status.Errors) == 0 {
2✔
783
                return errMsg
1✔
784
        }
1✔
785
        var errMap map[string]interface{}
1✔
786

1✔
787
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
788
                return errMsg
×
789
        }
×
790

791
        if description, found := errMap["description"]; found {
2✔
792
                if descStr, ok := description.(string); ok {
2✔
793
                        errMsg = descStr
1✔
794
                }
1✔
795
        }
796
        return errMsg
1✔
797
}
798

799
type SecretPredicate struct {
800
        predicate.Funcs
801
}
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