Skip to content

Commit 685dd6f

Browse files
Ensure broker deprovisions the instance on delete
* The OSBAPI client returns two flavours of errors - recoverable and unrecoverable * When deleting the `CFServiceInstance` its finalization routine would keep trying to deprovision on recoverable errors, or set the instance deprovision status to `failed` upon an unrecoverable error * Smoke and e2e tests are pushing the sample broker application to a dedicated org to ensure the broker is usable while deleting the org other sample apps are pushed to. fixes #3586 Co-authored-by: Danail Branekov <[email protected]>
1 parent 85e736f commit 685dd6f

File tree

12 files changed

+449
-229
lines changed

12 files changed

+449
-229
lines changed

controllers/api/v1alpha1/cfserviceinstance_types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ const (
3131

3232
CFServiceInstanceFinalizerName = "cfServiceInstance.korifi.cloudfoundry.org"
3333

34-
ProvisioningFailedCondition = "ProvisioningFailed"
34+
ProvisioningFailedCondition = "ProvisioningFailed"
35+
DeprovisioningFailedCondition = "DeprovisioningFailed"
3536
)
3637

3738
// CFServiceInstanceSpec defines the desired state of CFServiceInstance

controllers/controllers/services/instances/managed/controller.go

Lines changed: 142 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,20 @@ func (r *Reconciler) ReconcileResource(ctx context.Context, serviceInstance *kor
126126
serviceInstance.Status.ObservedGeneration = serviceInstance.Generation
127127
log.V(1).Info("set observed generation", "generation", serviceInstance.Status.ObservedGeneration)
128128

129+
serviceInstanceAssets, err := r.assets.GetServiceInstanceAssets(ctx, serviceInstance)
130+
if err != nil {
131+
log.Error(err, "failed to get service instance assets")
132+
return ctrl.Result{}, err
133+
}
134+
135+
osbapiClient, err := r.osbapiClientFactory.CreateClient(ctx, serviceInstanceAssets.ServiceBroker)
136+
if err != nil {
137+
log.Error(err, "failed to create broker client", "broker", serviceInstanceAssets.ServiceBroker.Name)
138+
return ctrl.Result{}, fmt.Errorf("failed to create client for broker %q: %w", serviceInstanceAssets.ServiceBroker.Name, err)
139+
}
140+
129141
if !serviceInstance.GetDeletionTimestamp().IsZero() {
130-
return r.finalizeCFServiceInstance(ctx, serviceInstance)
142+
return r.finalizeCFServiceInstance(ctx, serviceInstance, serviceInstanceAssets, osbapiClient)
131143
}
132144

133145
if isReady(serviceInstance) {
@@ -138,12 +150,6 @@ func (r *Reconciler) ReconcileResource(ctx context.Context, serviceInstance *kor
138150
return ctrl.Result{}, k8s.NewNotReadyError().WithReason("ProvisioningFailed").WithNoRequeue()
139151
}
140152

141-
serviceInstanceAssets, err := r.assets.GetServiceInstanceAssets(ctx, serviceInstance)
142-
if err != nil {
143-
log.Error(err, "failed to get service instance assets")
144-
return ctrl.Result{}, err
145-
}
146-
147153
planVisible, err := r.isServicePlanVisible(ctx, serviceInstance, serviceInstanceAssets.ServicePlan)
148154
if err != nil {
149155
log.Error(err, "failed to check service plan visibility")
@@ -159,52 +165,24 @@ func (r *Reconciler) ReconcileResource(ctx context.Context, serviceInstance *kor
159165
serviceInstance.Spec.ServiceLabel = tools.PtrTo(serviceInstanceAssets.ServiceOffering.Spec.Name)
160166
}
161167

162-
osbapiClient, err := r.osbapiClientFactory.CreateClient(ctx, serviceInstanceAssets.ServiceBroker)
163-
if err != nil {
164-
log.Error(err, "failed to create broker client", "broker", serviceInstanceAssets.ServiceBroker.Name)
165-
return ctrl.Result{}, fmt.Errorf("failed to create client for broker %q: %w", serviceInstanceAssets.ServiceBroker.Name, err)
166-
}
167-
168168
provisionResponse, err := r.provisionServiceInstance(ctx, serviceInstance, serviceInstanceAssets, osbapiClient)
169169
if err != nil {
170170
log.Error(err, "failed to provision service instance")
171171
return ctrl.Result{}, fmt.Errorf("failed to provision service instance: %w", err)
172172
}
173173

174174
if provisionResponse.IsAsync {
175-
return r.pollProvisionOperation(ctx, serviceInstance, serviceInstanceAssets, osbapiClient, provisionResponse.Operation)
175+
lastOpResponse, err := r.pollLastOperation(ctx, serviceInstance, serviceInstanceAssets, osbapiClient, provisionResponse.Operation)
176+
if err != nil {
177+
return ctrl.Result{}, err
178+
}
179+
return r.processProvisionOperation(serviceInstance, lastOpResponse)
176180
}
177181

178182
serviceInstance.Status.LastOperation.State = "succeeded"
179183
return ctrl.Result{}, nil
180184
}
181185

182-
func (r *Reconciler) isServicePlanVisible(
183-
ctx context.Context,
184-
serviceInstance *korifiv1alpha1.CFServiceInstance,
185-
servicePlan *korifiv1alpha1.CFServicePlan,
186-
) (bool, error) {
187-
if servicePlan.Spec.Visibility.Type == korifiv1alpha1.AdminServicePlanVisibilityType {
188-
return false, nil
189-
}
190-
191-
if servicePlan.Spec.Visibility.Type == korifiv1alpha1.PublicServicePlanVisibilityType {
192-
return true, nil
193-
}
194-
195-
namespace := &corev1.Namespace{
196-
ObjectMeta: metav1.ObjectMeta{
197-
Name: serviceInstance.Namespace,
198-
},
199-
}
200-
err := r.k8sClient.Get(ctx, client.ObjectKeyFromObject(namespace), namespace)
201-
if err != nil {
202-
return false, err
203-
}
204-
205-
return slices.Contains(servicePlan.Spec.Visibility.Organizations, namespace.Labels[korifiv1alpha1.OrgGUIDKey]), nil
206-
}
207-
208186
func (r *Reconciler) provisionServiceInstance(
209187
ctx context.Context,
210188
serviceInstance *korifiv1alpha1.CFServiceInstance,
@@ -265,31 +243,10 @@ func (r *Reconciler) provisionServiceInstance(
265243
return provisionResponse, nil
266244
}
267245

268-
func (r *Reconciler) pollProvisionOperation(
269-
ctx context.Context,
246+
func (r *Reconciler) processProvisionOperation(
270247
serviceInstance *korifiv1alpha1.CFServiceInstance,
271-
assets osbapi.ServiceInstanceAssets,
272-
osbapiClient osbapi.BrokerClient,
273-
operationID string,
248+
lastOpResponse osbapi.LastOperationResponse,
274249
) (ctrl.Result, error) {
275-
log := logr.FromContextOrDiscard(ctx).WithName("poll-provision-operation")
276-
277-
lastOpResponse, err := osbapiClient.GetServiceInstanceLastOperation(ctx, osbapi.GetServiceInstanceLastOperationRequest{
278-
InstanceID: serviceInstance.Name,
279-
GetLastOperationRequestParameters: osbapi.GetLastOperationRequestParameters{
280-
ServiceId: assets.ServiceOffering.Spec.BrokerCatalog.ID,
281-
PlanID: assets.ServicePlan.Spec.BrokerCatalog.ID,
282-
Operation: operationID,
283-
},
284-
})
285-
if err != nil {
286-
log.Error(err, "getting service instance last operation failed")
287-
return ctrl.Result{}, k8s.NewNotReadyError().WithCause(err).WithReason("GetLastOperationFailed")
288-
}
289-
290-
serviceInstance.Status.LastOperation.State = lastOpResponse.State
291-
serviceInstance.Status.LastOperation.Description = lastOpResponse.Description
292-
293250
if lastOpResponse.State == "in progress" {
294251
return ctrl.Result{}, k8s.NewNotReadyError().WithReason("ProvisionInProgress").WithRequeue()
295252
}
@@ -309,33 +266,31 @@ func (r *Reconciler) pollProvisionOperation(
309266
return ctrl.Result{}, nil
310267
}
311268

312-
func getServiceInstanceParameters(serviceInstance *korifiv1alpha1.CFServiceInstance) (map[string]any, error) {
313-
if serviceInstance.Spec.Parameters == nil {
314-
return nil, nil
315-
}
316-
317-
parametersMap := map[string]any{}
318-
err := json.Unmarshal(serviceInstance.Spec.Parameters.Raw, &parametersMap)
319-
if err != nil {
320-
return nil, fmt.Errorf("failed to unmarshal parameters: %w", err)
321-
}
322-
323-
return parametersMap, nil
324-
}
325-
326269
func (r *Reconciler) finalizeCFServiceInstance(
327270
ctx context.Context,
328271
serviceInstance *korifiv1alpha1.CFServiceInstance,
272+
assets osbapi.ServiceInstanceAssets,
273+
osbapiClient osbapi.BrokerClient,
329274
) (ctrl.Result, error) {
330275
log := logr.FromContextOrDiscard(ctx).WithName("finalizeCFServiceInstance")
331276

332277
if !controllerutil.ContainsFinalizer(serviceInstance, korifiv1alpha1.CFServiceInstanceFinalizerName) {
333278
return ctrl.Result{}, nil
334279
}
335280

336-
_, err := r.deprovisionServiceInstance(ctx, serviceInstance)
281+
deprovisionResponse, err := r.deprovisionServiceInstance(ctx, serviceInstance, assets, osbapiClient)
337282
if err != nil {
338283
log.Error(err, "failed to deprovision service instance with broker")
284+
return ctrl.Result{}, err
285+
}
286+
287+
if deprovisionResponse.IsAsync {
288+
289+
lastOpResponse, err := r.pollLastOperation(ctx, serviceInstance, assets, osbapiClient, deprovisionResponse.Operation)
290+
if err != nil {
291+
return ctrl.Result{}, err
292+
}
293+
return r.processDeprovisionOperation(serviceInstance, lastOpResponse)
339294
}
340295

341296
controllerutil.RemoveFinalizer(serviceInstance, korifiv1alpha1.CFServiceInstanceFinalizerName)
@@ -347,32 +302,130 @@ func (r *Reconciler) finalizeCFServiceInstance(
347302
func (r *Reconciler) deprovisionServiceInstance(
348303
ctx context.Context,
349304
serviceInstance *korifiv1alpha1.CFServiceInstance,
305+
assets osbapi.ServiceInstanceAssets,
306+
osbapiClient osbapi.BrokerClient,
350307
) (osbapi.ServiceInstanceOperationResponse, error) {
351-
assets, err := r.assets.GetServiceInstanceAssets(ctx, serviceInstance)
352-
if err != nil {
353-
return osbapi.ServiceInstanceOperationResponse{}, fmt.Errorf("failed to get service instance assets: %w", err)
354-
}
355-
356-
osbapiClient, err := r.osbapiClientFactory.CreateClient(ctx, assets.ServiceBroker)
357-
if err != nil {
358-
return osbapi.ServiceInstanceOperationResponse{}, fmt.Errorf("failed to create broker client: %w", err)
308+
serviceInstance.Status.LastOperation = services.LastOperation{
309+
Type: "delete",
310+
State: "initial",
359311
}
360-
361-
var deprovisionResponse osbapi.ServiceInstanceOperationResponse
362-
deprovisionResponse, err = osbapiClient.Deprovision(ctx, osbapi.InstanceDeprovisionPayload{
312+
deprovisionResponse, err := osbapiClient.Deprovision(ctx, osbapi.InstanceDeprovisionPayload{
363313
ID: serviceInstance.Name,
364314
InstanceDeprovisionRequest: osbapi.InstanceDeprovisionRequest{
365315
ServiceId: assets.ServiceOffering.Spec.BrokerCatalog.ID,
366316
PlanID: assets.ServicePlan.Spec.BrokerCatalog.ID,
367317
},
368318
})
369319
if err != nil {
320+
if osbapi.IsUnrecoveralbeError(err) {
321+
serviceInstance.Status.LastOperation.State = "failed"
322+
meta.SetStatusCondition(&serviceInstance.Status.Conditions, metav1.Condition{
323+
Type: korifiv1alpha1.DeprovisioningFailedCondition,
324+
Status: metav1.ConditionTrue,
325+
ObservedGeneration: serviceInstance.Generation,
326+
LastTransitionTime: metav1.NewTime(time.Now()),
327+
Reason: "DeprovisionFailed",
328+
Message: err.Error(),
329+
})
330+
return osbapi.ServiceInstanceOperationResponse{},
331+
k8s.NewNotReadyError().WithReason("DeprovisionFailed")
332+
}
333+
370334
return osbapi.ServiceInstanceOperationResponse{}, fmt.Errorf("failed to deprovision service instance: %w", err)
371335
}
372336

373337
return deprovisionResponse, nil
374338
}
375339

340+
func (r *Reconciler) processDeprovisionOperation(
341+
serviceInstance *korifiv1alpha1.CFServiceInstance,
342+
lastOpResponse osbapi.LastOperationResponse,
343+
) (ctrl.Result, error) {
344+
if lastOpResponse.State == "in progress" {
345+
return ctrl.Result{}, k8s.NewNotReadyError().WithReason("DeprovisionInProgress").WithRequeue()
346+
}
347+
348+
if lastOpResponse.State == "failed" {
349+
meta.SetStatusCondition(&serviceInstance.Status.Conditions, metav1.Condition{
350+
Type: korifiv1alpha1.DeprovisioningFailedCondition,
351+
Status: metav1.ConditionTrue,
352+
ObservedGeneration: serviceInstance.Generation,
353+
LastTransitionTime: metav1.NewTime(time.Now()),
354+
Reason: "DeprovisionFailed",
355+
Message: lastOpResponse.Description,
356+
})
357+
return ctrl.Result{}, k8s.NewNotReadyError().WithReason("DeprovisionFailed")
358+
}
359+
360+
return ctrl.Result{}, nil
361+
}
362+
363+
func (r *Reconciler) pollLastOperation(
364+
ctx context.Context,
365+
serviceInstance *korifiv1alpha1.CFServiceInstance,
366+
assets osbapi.ServiceInstanceAssets,
367+
osbapiClient osbapi.BrokerClient,
368+
operationID string,
369+
) (osbapi.LastOperationResponse, error) {
370+
log := logr.FromContextOrDiscard(ctx).WithName("poll-operation")
371+
lastOpResponse, err := osbapiClient.GetServiceInstanceLastOperation(ctx, osbapi.GetServiceInstanceLastOperationRequest{
372+
InstanceID: serviceInstance.Name,
373+
GetLastOperationRequestParameters: osbapi.GetLastOperationRequestParameters{
374+
ServiceId: assets.ServiceOffering.Spec.BrokerCatalog.ID,
375+
PlanID: assets.ServicePlan.Spec.BrokerCatalog.ID,
376+
Operation: operationID,
377+
},
378+
})
379+
if err != nil {
380+
log.Error(err, "getting service instance last operation failed")
381+
return osbapi.LastOperationResponse{}, k8s.NewNotReadyError().WithCause(err).WithReason("GetLastOperationFailed")
382+
}
383+
384+
serviceInstance.Status.LastOperation.State = lastOpResponse.State
385+
serviceInstance.Status.LastOperation.Description = lastOpResponse.Description
386+
return lastOpResponse, nil
387+
}
388+
389+
func getServiceInstanceParameters(serviceInstance *korifiv1alpha1.CFServiceInstance) (map[string]any, error) {
390+
if serviceInstance.Spec.Parameters == nil {
391+
return nil, nil
392+
}
393+
394+
parametersMap := map[string]any{}
395+
err := json.Unmarshal(serviceInstance.Spec.Parameters.Raw, &parametersMap)
396+
if err != nil {
397+
return nil, fmt.Errorf("failed to unmarshal parameters: %w", err)
398+
}
399+
400+
return parametersMap, nil
401+
}
402+
403+
func (r *Reconciler) isServicePlanVisible(
404+
ctx context.Context,
405+
serviceInstance *korifiv1alpha1.CFServiceInstance,
406+
servicePlan *korifiv1alpha1.CFServicePlan,
407+
) (bool, error) {
408+
if servicePlan.Spec.Visibility.Type == korifiv1alpha1.AdminServicePlanVisibilityType {
409+
return false, nil
410+
}
411+
412+
if servicePlan.Spec.Visibility.Type == korifiv1alpha1.PublicServicePlanVisibilityType {
413+
return true, nil
414+
}
415+
416+
namespace := &corev1.Namespace{
417+
ObjectMeta: metav1.ObjectMeta{
418+
Name: serviceInstance.Namespace,
419+
},
420+
}
421+
err := r.k8sClient.Get(ctx, client.ObjectKeyFromObject(namespace), namespace)
422+
if err != nil {
423+
return false, err
424+
}
425+
426+
return slices.Contains(servicePlan.Spec.Visibility.Organizations, namespace.Labels[korifiv1alpha1.OrgGUIDKey]), nil
427+
}
428+
376429
func (r *Reconciler) getNamespace(ctx context.Context, namespaceName string) (*corev1.Namespace, error) {
377430
namespace := &corev1.Namespace{
378431
ObjectMeta: metav1.ObjectMeta{

0 commit comments

Comments
 (0)