Skip to content
This repository was archived by the owner on Jun 2, 2021. It is now read-only.

Commit 5fad847

Browse files
authored
v3(services): unbind service credential binding of type app (cloudfoundry#1942)
* v3(services): Add NoAdditionalKey validation for get list for route bindings * WIP: Explicitly show the difference between MSI and UPSI in controller The "require_async" flag introduced decoupling from the concepts of MSI and USPI, however we can be more intentional in using the domain and use the SI types to distinguish between the behaviours * Remove unused method * Extraxted delete route binding job * Extracted delete binding action so that it can be extended by Route and Credential binding TODO: - add audit logging for service credential binding delete - correct behaviuor on "concurrency error" * Added audit events for service binding unbind Made sure to catch concurrency errors for service bindings
1 parent 311d3bf commit 5fad847

23 files changed

Lines changed: 1412 additions & 632 deletions

app/actions/service_credential_binding_delete.rb

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
1+
require 'actions/v3/service_binding_delete'
2+
13
module VCAP::CloudController
24
module V3
3-
class ServiceCredentialBindingDelete
4-
class NotImplementedError < StandardError
5+
class ServiceCredentialBindingDelete < V3::ServiceBindingDelete
6+
def initialize(user_audit_info)
7+
super()
8+
@user_audit_info = user_audit_info
59
end
610

7-
def delete(binding)
8-
not_implemented! if binding.service_instance.managed_instance?
11+
private
12+
13+
def perform_delete_actions(binding)
914
binding.destroy
15+
16+
event_repository.record_delete(binding, @user_audit_info)
1017
end
1118

12-
private
19+
def perform_start_delete_actions(binding)
20+
event_repository.record_start_delete(binding, @user_audit_info)
21+
end
1322

14-
def not_implemented!
15-
raise NotImplementedError.new
23+
def event_repository
24+
Repositories::ServiceBindingEventRepository
1625
end
1726
end
1827
end
Lines changed: 5 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,18 @@
1+
require 'services/service_brokers/service_client_provider'
2+
require 'actions/v3/service_binding_delete'
3+
14
module VCAP::CloudController
25
module V3
3-
class ServiceRouteBindingDelete
4-
class UnprocessableDelete < StandardError; end
5-
6-
class ConcurrencyError < StandardError; end
7-
8-
RequiresAsync = Class.new.freeze
9-
10-
DeleteStatus = Struct.new(:finished, :operation).freeze
11-
DeleteStarted = ->(operation) { DeleteStatus.new(false, operation) }
12-
DeleteComplete = DeleteStatus.new(true, nil).freeze
13-
14-
PollingStatus = Struct.new(:finished, :retry_after).freeze
15-
PollingFinished = PollingStatus.new(true, nil).freeze
16-
ContinuePolling = ->(retry_after) { PollingStatus.new(false, retry_after) }
17-
6+
class ServiceRouteBindingDelete < V3::ServiceBindingDelete
187
def initialize(service_event_repository)
8+
super()
199
@service_event_repository = service_event_repository
2010
end
2111

22-
def delete(binding, async_allowed:)
23-
operation_in_progress! if binding.service_instance.operation_in_progress?
24-
return RequiresAsync.new unless async_allowed || binding.service_instance.user_provided_instance?
25-
26-
result = send_unbind_to_broker(binding)
27-
if result[:finished]
28-
perform_delete_actions(binding)
29-
else
30-
update_last_operation(binding, operation: result[:operation])
31-
end
32-
33-
return result
34-
rescue => e
35-
unless e.is_a? ConcurrencyError
36-
update_last_operation(binding, state: 'failed', description: e.message)
37-
end
38-
39-
raise e
40-
end
41-
42-
def poll(binding)
43-
client = VCAP::Services::ServiceClientProvider.provide(instance: binding.service_instance)
44-
details = client.fetch_and_handle_service_binding_last_operation(binding)
45-
case details[:last_operation][:state]
46-
when 'in progress'
47-
update_last_operation(
48-
binding,
49-
description: details[:last_operation][:description],
50-
operation: binding.last_operation.broker_provided_operation)
51-
return ContinuePolling.call(details[:retry_after])
52-
when 'succeeded'
53-
perform_delete_actions(binding)
54-
return PollingFinished
55-
when 'failed'
56-
update_last_operation(binding, state: 'failed', description: details[:last_operation][:description])
57-
raise LastOperationFailedState
58-
end
59-
rescue LastOperationFailedState => e
60-
raise e
61-
rescue => e
62-
update_last_operation(binding, state: 'failed', description: e.message)
63-
raise e
64-
end
65-
6612
private
6713

6814
attr_reader :service_event_repository
6915

70-
def send_unbind_to_broker(binding)
71-
client = VCAP::Services::ServiceClientProvider.provide(instance: binding.service_instance)
72-
details = client.unbind(binding, nil, true)
73-
details[:async] ? DeleteStarted.call(details[:operation]) : DeleteComplete
74-
rescue VCAP::Services::ServiceBrokers::V2::Errors::ConcurrencyError
75-
raise ConcurrencyError.new(
76-
'The service broker rejected the request due to an operation being in progress for the service route binding'
77-
)
78-
rescue => err
79-
raise UnprocessableDelete.new("Service broker failed to delete service binding for instance #{binding.service_instance.name}: #{err.message}")
80-
end
81-
8216
def perform_delete_actions(binding)
8317
record_audit_event(binding)
8418
binding.destroy
@@ -92,19 +26,6 @@ def record_audit_event(binding)
9226
{ route_guid: binding.route.guid },
9327
)
9428
end
95-
96-
def update_last_operation(binding, description: nil, state: 'in progress', operation: nil)
97-
binding.save_with_new_operation({}, {
98-
type: 'delete',
99-
state: state,
100-
description: description,
101-
broker_provided_operation: operation || binding.last_operation.broker_provided_operation
102-
})
103-
end
104-
105-
def operation_in_progress!
106-
raise UnprocessableDelete.new('There is an operation in progress for the service instance')
107-
end
10829
end
10930
end
11031
end

app/actions/v3/service_binding_create.rb

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -94,22 +94,6 @@ def bindings_retrievable?(binding)
9494
def not_retrievable!
9595
raise BindingNotRetrievable.new('The broker responded asynchronously but does not support fetching binding data')
9696
end
97-
98-
def fetch_last_operation(client, binding)
99-
client.fetch_service_binding_last_operation(binding)
100-
rescue VCAP::Services::ServiceBrokers::V2::Errors::ServiceBrokerBadResponse,
101-
VCAP::Services::ServiceBrokers::V2::Errors::ServiceBrokerRequestRejected,
102-
HttpRequestError => e
103-
binding.save_with_attributes_and_new_operation(
104-
{},
105-
{
106-
type: 'create',
107-
state: 'in progress',
108-
description: e.message,
109-
})
110-
111-
return nil
112-
end
11397
end
11498
end
11599
end
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
require 'services/service_brokers/service_client_provider'
2+
3+
module VCAP::CloudController
4+
module V3
5+
class ServiceBindingDelete
6+
class UnprocessableDelete < StandardError; end
7+
8+
class ConcurrencyError < StandardError; end
9+
class OperationCancelled < StandardError; end
10+
11+
DeleteStatus = Struct.new(:finished, :operation).freeze
12+
DeleteStarted = ->(operation) { DeleteStatus.new(false, operation) }
13+
DeleteComplete = DeleteStatus.new(true, nil).freeze
14+
15+
PollingStatus = Struct.new(:finished, :retry_after).freeze
16+
PollingFinished = PollingStatus.new(true, nil).freeze
17+
ContinuePolling = ->(retry_after) { PollingStatus.new(false, retry_after) }
18+
19+
def delete(binding)
20+
result = send_unbind_to_client(binding)
21+
if result[:finished]
22+
perform_delete_actions(binding)
23+
else
24+
perform_start_delete_actions(binding)
25+
update_last_operation(binding, operation: result[:operation])
26+
end
27+
28+
return result
29+
rescue => e
30+
unless e.is_a? ConcurrencyError
31+
update_last_operation(binding, state: 'failed', description: e.message)
32+
end
33+
34+
raise e
35+
end
36+
37+
def poll(binding)
38+
client = VCAP::Services::ServiceClientProvider.provide(instance: binding.service_instance)
39+
details = client.fetch_and_handle_service_binding_last_operation(binding)
40+
41+
case details[:last_operation][:state]
42+
when 'in progress'
43+
update_last_operation(
44+
binding,
45+
description: details[:last_operation][:description],
46+
operation: binding.last_operation.broker_provided_operation)
47+
return ContinuePolling.call(details[:retry_after])
48+
when 'succeeded'
49+
perform_delete_actions(binding)
50+
return PollingFinished
51+
when 'failed'
52+
update_last_operation(binding, state: 'failed', description: details[:last_operation][:description])
53+
raise LastOperationFailedState
54+
end
55+
rescue LastOperationFailedState => e
56+
raise e
57+
rescue => e
58+
update_last_operation(binding, state: 'failed', description: e.message)
59+
raise e
60+
end
61+
62+
class BindingNotRetrievable < StandardError; end
63+
64+
private
65+
66+
def perform_start_delete_actions(binding); end
67+
68+
def send_unbind_to_client(binding)
69+
client = VCAP::Services::ServiceClientProvider.provide(instance: binding.service_instance)
70+
details = client.unbind(binding, nil, true)
71+
details[:async] ? DeleteStarted.call(details[:operation]) : DeleteComplete
72+
rescue VCAP::Services::ServiceBrokers::V2::Errors::ConcurrencyError
73+
raise ConcurrencyError.new(
74+
'The service broker rejected the request due to an operation being in progress for the service route binding'
75+
)
76+
rescue CloudController::Errors::ApiError => err
77+
if err.name == 'AsyncServiceBindingOperationInProgress'
78+
raise ConcurrencyError.new(
79+
'The service broker rejected the request due to an operation being in progress for the service route binding'
80+
)
81+
end
82+
raise unprocessable!(binding, err)
83+
rescue => err
84+
raise unprocessable!(binding, err)
85+
end
86+
87+
def unprocessable!(binding, err)
88+
UnprocessableDelete.new("Service broker failed to delete service binding for instance #{binding.service_instance.name}: #{err.message}")
89+
end
90+
91+
def update_last_operation(binding, description: nil, state: 'in progress', operation: nil)
92+
binding.save_with_attributes_and_new_operation(
93+
{},
94+
{
95+
type: 'delete',
96+
state: state,
97+
description: description,
98+
broker_provided_operation: operation || binding.last_operation&.broker_provided_operation
99+
})
100+
end
101+
end
102+
end
103+
end

app/controllers/v3/service_credential_bindings_controller.rb

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
require 'decorators/include_binding_app_decorator'
1111
require 'decorators/include_binding_service_instance_decorator'
1212
require 'jobs/v3/create_service_credential_binding_job_actor'
13+
require 'jobs/v3/delete_binding_job'
1314

1415
class ServiceCredentialBindingsController < ApplicationController
1516
def index
@@ -68,10 +69,22 @@ def destroy
6869
not_found! unless service_credential_binding.present?
6970
unauthorized! unless can_write_to_space?(binding_space)
7071

71-
V3::ServiceCredentialBindingDelete.new.delete(service_credential_binding)
72-
head :no_content
73-
rescue V3::ServiceCredentialBindingDelete::NotImplementedError
74-
head :not_implemented
72+
if service_credential_binding.is_a?(ServiceKey)
73+
head :not_implemented
74+
return
75+
end
76+
77+
operation_in_progress! if service_credential_binding.service_instance.operation_in_progress?
78+
79+
case service_credential_binding.service_instance
80+
when ManagedServiceInstance
81+
pollable_job_guid = enqueue_unbind_job(service_credential_binding.guid)
82+
head :accepted, 'Location' => url_builder.build_url(path: "/v3/jobs/#{pollable_job_guid}")
83+
when UserProvidedServiceInstance
84+
action = V3::ServiceCredentialBindingDelete.new(user_audit_info)
85+
action.delete(service_credential_binding)
86+
head :no_content
87+
end
7588
end
7689

7790
def details
@@ -121,6 +134,16 @@ def enqueue_bind_job(binding_guid, message)
121134
pollable_job.guid
122135
end
123136

137+
def enqueue_unbind_job(binding_guid)
138+
bind_job = VCAP::CloudController::V3::DeleteBindingJob.new(
139+
:credential,
140+
binding_guid,
141+
user_audit_info: user_audit_info,
142+
)
143+
pollable_job = Jobs::Enqueuer.new(bind_job, queue: Jobs::Queues.generic).enqueue_pollable
144+
pollable_job.guid
145+
end
146+
124147
def resource_not_accessible!(resource, guid)
125148
unprocessable!("The #{resource} could not be found: '#{guid}'")
126149
end
@@ -195,10 +218,6 @@ def ensure_service_credential_binding_is_accessible!
195218
not_found! unless service_credential_binding_exists?
196219
end
197220

198-
def not_found!
199-
resource_not_found!(:service_credential_binding)
200-
end
201-
202221
def service_credential_binding_exists?
203222
!!service_credential_binding
204223
end
@@ -230,4 +249,12 @@ def fetcher
230249
def query_params
231250
request.query_parameters.with_indifferent_access
232251
end
252+
253+
def operation_in_progress!
254+
unprocessable!('There is an operation in progress for the service instance.')
255+
end
256+
257+
def not_found!
258+
resource_not_found!(:service_credential_binding)
259+
end
233260
end

app/controllers/v3/service_route_bindings_controller.rb

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
require 'actions/service_route_binding_create'
55
require 'actions/service_route_binding_delete'
66
require 'jobs/v3/create_binding_async_job'
7-
require 'jobs/v3/delete_route_binding_job'
7+
require 'jobs/v3/delete_binding_job'
88
require 'presenters/v3/paginated_list_presenter'
99
require 'presenters/v3/service_route_binding_presenter'
1010
require 'fetchers/route_binding_list_fetcher'
@@ -64,14 +64,15 @@ def create
6464

6565
def destroy
6666
route_binding_not_found! unless @route_binding && can_read_space?(@route_binding.route.space)
67+
operation_in_progress! if @route_binding.service_instance.operation_in_progress?
6768

68-
action = V3::ServiceRouteBindingDelete.new(service_event_repository)
69-
result = action.delete(@route_binding, async_allowed: false)
70-
71-
if result.is_a? V3::ServiceRouteBindingDelete::RequiresAsync
69+
case @route_binding.service_instance
70+
when ManagedServiceInstance
7271
pollable_job_guid = enqueue_unbind_job(@route_binding.guid)
7372
head :accepted, 'Location' => url_builder.build_url(path: "/v3/jobs/#{pollable_job_guid}")
74-
else
73+
when UserProvidedServiceInstance
74+
action = V3::ServiceRouteBindingDelete.new(service_event_repository)
75+
action.delete(@route_binding)
7576
head :no_content
7677
end
7778
rescue V3::ServiceRouteBindingDelete::UnprocessableDelete => e
@@ -144,7 +145,8 @@ def enqueue_bind_job(binding_guid, parameters)
144145
end
145146

146147
def enqueue_unbind_job(binding_guid)
147-
bind_job = VCAP::CloudController::V3::DeleteRouteBindingJob.new(
148+
bind_job = VCAP::CloudController::V3::DeleteBindingJob.new(
149+
:route,
148150
binding_guid,
149151
user_audit_info: user_audit_info,
150152
)
@@ -237,6 +239,10 @@ def already_exists!
237239
raise CloudController::Errors::ApiError.new_from_details('ServiceInstanceAlreadyBoundToSameRoute').with_response_code(422)
238240
end
239241

242+
def operation_in_progress!
243+
unprocessable!('There is an operation in progress for the service instance.')
244+
end
245+
240246
def set_route_binding
241247
@route_binding = RouteBinding.first(guid: hashed_params[:guid])
242248
end

0 commit comments

Comments
 (0)