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

SAP / sap-btp-service-operator / 20781332420

07 Jan 2026 12:22PM UTC coverage: 77.73% (-0.7%) from 78.436%
20781332420

Pull #593

github

web-flow
Merge branch 'main' into move
Pull Request #593: validate resource exists

8 of 44 new or added lines in 2 files covered. (18.18%)

5 existing lines in 1 file now uncovered.

2740 of 3525 relevant lines covered (77.73%)

0.88 hits per line

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

78.17
/controllers/serviceinstance_controller.go
1
/*
2

3

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

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

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

17
package controllers
18

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

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

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

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

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

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

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

47
        "github.com/google/uuid"
48

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

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

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

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

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

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

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

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

126
        // If stored hash is MD5 (32 chars) and we're now using SHA256 (64 chars),
127
        // perform one-time migration by updating the stored hash without triggering update
128
        if len(serviceInstance.Status.HashedSpec) == 32 {
2✔
129
                // This is likely an MD5->SHA256 migration, update the stored hash silently
1✔
130
                // to prevent unnecessary service updates during FIPS migration
1✔
131
                log.Info(fmt.Sprintf("updated hashing for instance '%s' (id=%s)", serviceInstance.Name, serviceInstance.Status.InstanceID))
1✔
132
                updateHashedSpecValue(serviceInstance)
1✔
133
                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
134
        }
1✔
135

136
        if len(serviceInstance.Status.OperationURL) > 0 {
2✔
137
                // ongoing operation - poll status from SM
1✔
138
                return r.poll(ctx, serviceInstance)
1✔
139
        }
1✔
140

141
        if isFinalState(ctx, serviceInstance) {
2✔
142
                if len(serviceInstance.Status.HashedSpec) == 0 {
2✔
143
                        updateHashedSpecValue(serviceInstance)
1✔
144
                        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
145
                }
1✔
146

147
                return ctrl.Result{}, nil
1✔
148
        }
149

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

321
                if len(serviceInstance.Status.OperationURL) > 0 && serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
322
                        // ongoing delete operation - poll status from SM
1✔
323
                        return r.poll(ctx, serviceInstance)
1✔
324
                }
1✔
325

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

332
                if operationURL != "" {
2✔
333
                        log.Info("Deleting instance async")
1✔
334
                        return r.handleAsyncDelete(ctx, serviceInstance, operationURL)
1✔
335
                }
1✔
336

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

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

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

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

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

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

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

452
        serviceInstance.Status.OperationURL = ""
1✔
453
        serviceInstance.Status.OperationType = ""
1✔
454

1✔
455
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
456
}
457

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

1✔
463
        if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
1✔
464
                return ctrl.Result{}, err
×
465
        }
×
466

467
        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
468
}
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
                utils.SetInProgressConditions(ctx, smInstance.LastOperation.Type, smInstance.LastOperation.Description, k8sInstance, false)
1✔
535
        case smClientTypes.SUCCEEDED:
1✔
536
                utils.SetSuccessConditions(operationType, k8sInstance, false)
1✔
537
        case smClientTypes.FAILED:
1✔
538
                utils.SetFailureConditions(operationType, description, k8sInstance, false)
1✔
539
        }
540

541
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, k8sInstance)
1✔
542
}
543

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

562
                        if err := utils.AddWatchForSecretIfNeeded(ctx, r.Client, secret, string(serviceInstance.UID)); err != nil {
1✔
563
                                log.Error(err, fmt.Sprintf("failed to mark secret for watch %s", secret.Name))
×
564
                                return nil, err
×
565
                        }
×
566

567
                }
568
        }
569

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

592
        return instanceParameters, nil
1✔
593
}
594

595
func isFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) bool {
1✔
596
        log := logutils.GetLogger(ctx)
1✔
597

1✔
598
        if serviceInstance.Status.ForceReconcile {
2✔
599
                log.Info("instance is not in final state, ForceReconcile is true")
1✔
600
                return false
1✔
601
        }
1✔
602

603
        observedGen := common.GetObservedGeneration(serviceInstance)
1✔
604
        if serviceInstance.Generation != observedGen {
2✔
605
                log.Info(fmt.Sprintf("instance is not in final state, generation: %d, observedGen: %d", serviceInstance.Generation, observedGen))
1✔
606
                return false
1✔
607
        }
1✔
608

609
        if utils.ShouldRetryOperation(serviceInstance) {
2✔
610
                log.Info("instance is not in final state, last operation failed, retrying")
1✔
611
                return false
1✔
612
        }
1✔
613

614
        if shareOrUnshareRequired(serviceInstance) {
2✔
615
                log.Info("instance is not in final state, need to sync sharing status")
1✔
616
                if len(serviceInstance.Status.HashedSpec) == 0 {
1✔
617
                        updateHashedSpecValue(serviceInstance)
×
618
                }
×
619
                return false
1✔
620
        }
621

622
        log.Info(fmt.Sprintf("instance is in final state (generation: %d)", serviceInstance.Generation))
1✔
623
        return true
1✔
624
}
625

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

632
        if serviceInstance.Status.ForceReconcile {
2✔
633
                return true
1✔
634
        }
1✔
635

636
        cond := meta.FindStatusCondition(serviceInstance.Status.Conditions, common.ConditionSucceeded)
1✔
637
        if cond != nil && cond.Reason == common.UpdateInProgress { //in case of transient error occurred
1✔
638
                return true
×
639
        }
×
640

641
        return serviceInstance.GetSpecHash() != serviceInstance.Status.HashedSpec
1✔
642
}
643

644
func shareOrUnshareRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
645
        //relevant only for non-shared instances - sharing instance is possible only for usable instances
1✔
646
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
2✔
647
                return false
1✔
648
        }
1✔
649

650
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), common.ConditionShared)
1✔
651
        if sharedCondition == nil {
2✔
652
                return serviceInstance.GetShared()
1✔
653
        }
1✔
654

655
        if sharedCondition.Reason == common.ShareNotSupported {
2✔
656
                return false
1✔
657
        }
1✔
658

659
        if sharedCondition.Status == metav1.ConditionFalse {
2✔
660
                // instance does not appear to be shared, should share it if shared is requested
1✔
661
                return serviceInstance.GetShared()
1✔
662
        }
1✔
663

664
        // instance appears to be shared, should unshare it if shared is not requested
665
        return !serviceInstance.GetShared()
1✔
666
}
667

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

677
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
678
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
679
        }
1✔
680

681
        offeringQuery := &sm.Parameters{
×
682
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
683
        }
×
684

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

693
        var tags []string
×
694
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
695
                return nil, err
×
696
        }
×
697
        return tags, nil
×
698
}
699

700
func getTags(tags []byte) ([]string, error) {
1✔
701
        var tagsArr []string
1✔
702
        if err := json.Unmarshal(tags, &tagsArr); err != nil {
1✔
703
                return nil, err
×
704
        }
×
705
        return tagsArr, nil
1✔
706
}
707

708
func updateHashedSpecValue(serviceInstance *v1.ServiceInstance) {
1✔
709
        serviceInstance.Status.HashedSpec = serviceInstance.GetSpecHash()
1✔
710
}
1✔
711

712
func getErrorMsgFromLastOperation(status *smClientTypes.Operation) string {
1✔
713
        errMsg := "async operation error"
1✔
714
        if status == nil || len(status.Errors) == 0 {
1✔
715
                return errMsg
×
716
        }
×
717
        var errMap map[string]interface{}
1✔
718

1✔
719
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
720
                return errMsg
×
721
        }
×
722

723
        if description, found := errMap["description"]; found {
2✔
724
                if descStr, ok := description.(string); ok {
2✔
725
                        errMsg = descStr
1✔
726
                }
1✔
727
        }
728
        return errMsg
1✔
729
}
730

731
type SecretPredicate struct {
732
        predicate.Funcs
733
}
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