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

SAP / sap-btp-service-operator / 25664426865

11 May 2026 10:22AM UTC coverage: 77.719% (-0.6%) from 78.279%
25664426865

push

github

web-flow
[INFRA] update dependencies (#633)

* [INFRA] update dependencies

* fix lint

---------

Co-authored-by: I501080 <keren.lahav@sap.com>

0 of 20 new or added lines in 2 files covered. (0.0%)

4 existing lines in 3 files now uncovered.

2815 of 3622 relevant lines covered (77.72%)

0.88 hits per line

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

81.98
/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✔
93
                log.Error(err, "failed to get sm client")
×
94
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, common.Unknown, err)
×
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, smClient, serviceInstance)
1✔
101
        }
1✔
102

103
        if shouldInstanceBeDeleted(serviceInstance) {
2✔
104
                return r.deleteInstance(ctx, smClient, serviceInstance)
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 {
1✔
UNCOV
277
                        return ctrl.Result{}, err
×
UNCOV
278
                }
×
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, smClient sm.Client, serviceInstance *v1.ServiceInstance) (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, smClient sm.Client, serviceInstance *v1.ServiceInstance) (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
                if !serviceInstance.IsAsyncProvisionFailed() { // do not override original error message in case of failed async instance deletion error
2✔
417
                        utils.SetFailureConditions(status.Type, errMsg, serviceInstance, true)
1✔
418
                }
1✔
419
                if serviceInstance.Status.OperationType == smClientTypes.CREATE {
2✔
420
                        log.Info(fmt.Sprintf("async provision failed for instance %s", serviceInstance.Status.InstanceID))
1✔
421
                        trueVal := true
1✔
422
                        serviceInstance.Status.AsyncProvisionFailed = &trueVal
1✔
423
                }
1✔
424
                serviceInstance.Status.OperationURL = ""
1✔
425
                serviceInstance.Status.OperationType = ""
1✔
426
                if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
2✔
427
                        return ctrl.Result{}, err
1✔
428
                }
1✔
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

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

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

1✔
467
        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
468
}
1✔
469

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

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

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

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

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

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

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

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

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

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

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

596
        return instanceParameters, nil
1✔
597
}
598

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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