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

Commit a044502

Browse files
blgmDerik Evangelista
andauthored
v3(services): delete a route binding **sync** (cloudfoundry#1868)
[#174191028](https://www.pivotaltracker.com/story/show/174191028) Co-authored-by: Derik Evangelista <devangelista@pivotal.io>
1 parent c83d293 commit a044502

8 files changed

Lines changed: 521 additions & 8 deletions
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
module VCAP::CloudController
2+
module V3
3+
class ServiceRouteBindingDelete
4+
class UnprocessableDelete < StandardError; end
5+
6+
RequiresAsync = Class.new.freeze
7+
DeleteComplete = Class.new.freeze
8+
DeleteInProgress = Struct.new(:operation).freeze
9+
10+
def initialize(service_event_repository)
11+
@service_event_repository = service_event_repository
12+
end
13+
14+
def delete(binding, async_allowed:)
15+
return RequiresAsync unless async_allowed || binding.service_instance.user_provided_instance?
16+
17+
operation_in_progress! if binding.service_instance.operation_in_progress?
18+
19+
result = send_unbind_to_broker(binding)
20+
unsupported! unless result == DeleteComplete
21+
22+
record_audit_event(binding)
23+
binding.destroy
24+
binding.notify_diego
25+
26+
DeleteComplete
27+
end
28+
29+
private
30+
31+
attr_reader :service_event_repository
32+
33+
def send_unbind_to_broker(binding)
34+
client = VCAP::Services::ServiceClientProvider.provide(instance: binding.service_instance)
35+
details = client.unbind(binding, accepts_incomplete: false)
36+
details[:async] ? DeleteInProgress.new(details[:operation]) : DeleteComplete
37+
rescue => err
38+
raise UnprocessableDelete.new("Service broker failed to delete service binding for instance #{binding.service_instance.name}: #{err.message}")
39+
end
40+
41+
def record_audit_event(binding)
42+
service_event_repository.record_service_instance_event(
43+
:unbind_route,
44+
binding.service_instance,
45+
{ route_guid: binding.route.guid },
46+
)
47+
end
48+
49+
def operation_in_progress!
50+
raise UnprocessableDelete.new('There is an operation in progress for the service instance')
51+
end
52+
53+
def unsupported!
54+
raise UnprocessableDelete.new('async unbind not supported yet')
55+
end
56+
end
57+
end
58+
end

app/controllers/v3/service_route_bindings_controller.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
require 'messages/service_route_binding_show_message'
33
require 'messages/service_route_bindings_list_message'
44
require 'actions/service_route_binding_create'
5+
require 'actions/service_route_binding_delete'
56
require 'jobs/v3/create_route_binding_job'
7+
require 'jobs/v3/delete_route_binding_job'
68
require 'presenters/v3/paginated_list_presenter'
79
require 'presenters/v3/service_route_binding_presenter'
810
require 'fetchers/route_binding_list_fetcher'
@@ -59,6 +61,21 @@ def index
5961
)
6062
end
6163

64+
def destroy
65+
route_binding = RouteBinding.first(guid: hashed_params[:guid])
66+
route_binding_not_found! unless route_binding && can_read_space?(route_binding.route.space)
67+
68+
action = V3::ServiceRouteBindingDelete.new(service_event_repository)
69+
result = action.delete(route_binding, async_allowed: false)
70+
71+
if result == V3::ServiceRouteBindingDelete::RequiresAsync
72+
pollable_job_guid = enqueue_unbind_job(route_binding.guid)
73+
head :accepted, 'Location' => url_builder.build_url(path: "/v3/jobs/#{pollable_job_guid}")
74+
else
75+
head :no_content
76+
end
77+
end
78+
6279
private
6380

6481
AVAILABLE_DECORATORS = [
@@ -94,6 +111,15 @@ def enqueue_bind_job(binding_guid, parameters)
94111
pollable_job.guid
95112
end
96113

114+
def enqueue_unbind_job(binding_guid)
115+
bind_job = VCAP::CloudController::V3::DeleteRouteBindingJob.new(
116+
binding_guid,
117+
user_audit_info: user_audit_info,
118+
)
119+
pollable_job = Jobs::Enqueuer.new(bind_job, queue: Jobs::Queues.generic).enqueue_pollable
120+
pollable_job.guid
121+
end
122+
97123
def fetch_route_bindings(message)
98124
fetcher = RouteBindingListFetcher.new
99125
if permission_queryer.can_read_globally?
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
require 'jobs/reoccurring_job'
2+
require 'actions/service_route_binding_create'
3+
require 'cloud_controller/errors/api_error'
4+
5+
module VCAP::CloudController
6+
module V3
7+
class DeleteRouteBindingJob < VCAP::CloudController::Jobs::CCJob
8+
def initialize(binding_guid, user_audit_info:)
9+
super()
10+
@binding_guid = binding_guid
11+
@user_audit_info = user_audit_info
12+
end
13+
14+
def operation
15+
:unbind
16+
end
17+
18+
def operation_type
19+
'delete'
20+
end
21+
22+
def max_attempts
23+
1
24+
end
25+
26+
def display_name
27+
'service_route_bindings.delete'
28+
end
29+
30+
def resource_guid
31+
@binding_guid
32+
end
33+
34+
def resource_type
35+
'service_route_binding'
36+
end
37+
38+
def perform
39+
binding = route_binding
40+
gone! unless binding
41+
42+
service_event_repository = VCAP::CloudController::Repositories::ServiceEventRepository::WithUserActor.new(@user_audit_info)
43+
action = V3::ServiceRouteBindingDelete.new(service_event_repository)
44+
action.delete(binding, async_allowed: true)
45+
rescue => e
46+
raise CloudController::Errors::ApiError.new_from_details('UnableToPerform', 'unbind', e.message)
47+
end
48+
49+
private
50+
51+
def route_binding
52+
RouteBinding.first(guid: resource_guid)
53+
end
54+
55+
def gone!
56+
raise CloudController::Errors::ApiError.new_from_details('ResourceNotFound', "The binding could not be found: #{resource_guid}")
57+
end
58+
end
59+
end
60+
end

config/routes.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@
197197
# service_route_bindings
198198
resources :service_route_bindings,
199199
param: :guid,
200-
only: [:show, :create, :index]
200+
only: [:show, :create, :index, :destroy]
201201

202202
# service_brokers
203203
get '/service_brokers', to: 'service_brokers#index'

spec/request/service_route_bindings_spec.rb

Lines changed: 150 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@
218218
get "/v3/jobs/#{job.guid}", nil, space_dev_headers
219219

220220
expect(last_response).to have_status_code(200)
221-
expect(parsed_response['guid']). to eq(job.guid)
221+
expect(parsed_response['guid']).to eq(job.guid)
222222
end
223223

224224
describe 'the pollable job' do
@@ -650,11 +650,11 @@
650650
[
651651
expected_json(
652652
binding_guid: route_binding_1.guid,
653-
route_service_url: route_service_url,
654-
service_instance_guid: service_instance_1.guid,
655-
route_guid: route.guid,
656-
last_operation_type: 'create',
657-
last_operation_state: 'successful',
653+
route_service_url: route_service_url,
654+
service_instance_guid: service_instance_1.guid,
655+
route_guid: route.guid,
656+
last_operation_type: 'create',
657+
last_operation_state: 'successful',
658658
),
659659
expected_json(
660660
binding_guid: route_binding_2.guid,
@@ -867,6 +867,149 @@
867867
end
868868
end
869869

870+
describe 'DELETE /v3/service_route_bindings/:guid' do
871+
let(:api_call) { lambda { |user_headers| delete "/v3/service_route_bindings/#{guid}", nil, user_headers } }
872+
873+
context 'route binding exists' do
874+
let(:route) { VCAP::CloudController::Route.make(space: space) }
875+
let(:binding) do
876+
VCAP::CloudController::RouteBinding.new.save_with_new_operation(
877+
{ service_instance: service_instance, route: route, route_service_url: route_service_url },
878+
{ type: 'create', state: 'successful' }
879+
)
880+
end
881+
let(:guid) { binding.guid }
882+
883+
context 'user-provided service instance' do
884+
let(:service_instance) { VCAP::CloudController::UserProvidedServiceInstance.make(space: space, route_service_url: route_service_url) }
885+
886+
let(:expected_codes_and_responses) { responses_for_space_restricted_delete_endpoint }
887+
let(:db_check) {
888+
lambda do
889+
expect(VCAP::CloudController::RouteBinding.all).to be_empty
890+
end
891+
}
892+
893+
it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS
894+
end
895+
896+
context 'managed service instance' do
897+
let(:service_offering) { VCAP::CloudController::Service.make(requires: ['route_forwarding']) }
898+
let(:service_plan) { VCAP::CloudController::ServicePlan.make(service: service_offering) }
899+
let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space: space, service_plan: service_plan) }
900+
901+
let(:expected_codes_and_responses) { responses_for_space_restricted_async_delete_endpoint }
902+
let(:db_check) { lambda {} }
903+
let(:job) { VCAP::CloudController::PollableJobModel.last }
904+
905+
it_behaves_like 'permissions for delete endpoint', ALL_PERMISSIONS
906+
907+
it 'responds with a job resource' do
908+
api_call.call(space_dev_headers)
909+
expect(last_response).to have_status_code(202)
910+
expect(last_response.headers['Location']).to end_with("/v3/jobs/#{job.guid}")
911+
912+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::PROCESSING_STATE)
913+
expect(job.operation).to eq('service_route_bindings.delete')
914+
expect(job.resource_guid).to eq(binding.guid)
915+
expect(job.resource_type).to eq('service_route_binding')
916+
917+
get "/v3/jobs/#{job.guid}", nil, space_dev_headers
918+
919+
expect(last_response).to have_status_code(200)
920+
expect(parsed_response['guid']).to eq(job.guid)
921+
end
922+
923+
describe 'the pollable job' do
924+
let(:broker_base_url) { service_instance.service_broker.broker_url }
925+
let(:broker_unbind_url) { "#{broker_base_url}/v2/service_instances/#{service_instance.guid}/service_bindings/#{binding.guid}" }
926+
let(:route_service_url) { 'https://route_service_url.com' }
927+
let(:broker_status_code) { 200 }
928+
let(:broker_response) { {} }
929+
let(:query) do
930+
{
931+
service_id: service_instance.service_plan.service.unique_id,
932+
plan_id: service_instance.service_plan.unique_id,
933+
}
934+
end
935+
936+
before do
937+
api_call.call(space_dev_headers)
938+
expect(last_response).to have_status_code(202)
939+
940+
stub_request(:delete, broker_unbind_url).
941+
with(query: query).
942+
to_return(status: broker_status_code, body: broker_response.to_json, headers: {})
943+
end
944+
945+
it 'sends an unbind request with the right arguments to the service broker' do
946+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
947+
948+
expect(
949+
a_request(:delete, broker_unbind_url).
950+
with(
951+
query: query,
952+
)
953+
).to have_been_made.once
954+
end
955+
956+
context 'when the unbind completes synchronously' do
957+
it 'removes the binding' do
958+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
959+
960+
expect(VCAP::CloudController::RouteBinding.all).to be_empty
961+
end
962+
963+
it 'completes the job' do
964+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
965+
966+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::COMPLETE_STATE)
967+
end
968+
end
969+
970+
context 'when the broker returns a failure' do
971+
let(:broker_status_code) { 418 }
972+
let(:broker_response) { 'nope' }
973+
974+
it 'does not remove the binding' do
975+
execute_all_jobs(expected_successes: 0, expected_failures: 1)
976+
977+
expect(VCAP::CloudController::RouteBinding.all).not_to be_empty
978+
end
979+
980+
it 'puts the error details in the job' do
981+
execute_all_jobs(expected_successes: 0, expected_failures: 1)
982+
983+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)
984+
expect(job.cf_api_error).not_to be_nil
985+
error = YAML.safe_load(job.cf_api_error)
986+
expect(error['errors'].first['code']).to eq(10009)
987+
expect(error['errors'].first['detail']).
988+
to include('The service broker rejected the request. Status Code: 418 I\'m a Teapot, Body: "nope"')
989+
end
990+
end
991+
end
992+
end
993+
end
994+
995+
context 'no route binding' do
996+
let(:guid) { 'no-such-route-binding' }
997+
998+
it 'fails with the correct error' do
999+
api_call.call(space_dev_headers)
1000+
1001+
expect(last_response).to have_status_code(404)
1002+
expect(parsed_response['errors']).to include(
1003+
include({
1004+
'detail' => 'Service route binding not found',
1005+
'title' => 'CF-ResourceNotFound',
1006+
'code' => 10010,
1007+
})
1008+
)
1009+
end
1010+
end
1011+
end
1012+
8701013
let(:user) { VCAP::CloudController::User.make }
8711014
let(:org) { VCAP::CloudController::Organization.make }
8721015
let(:space) { VCAP::CloudController::Space.make(organization: org) }
@@ -921,7 +1064,7 @@ def bind_service_to_route(service_instance, route)
9211064
route_service_url = service_instance.route_service_url
9221065
VCAP::CloudController::RouteBinding.new.save_with_new_operation(
9231066
{ service_instance: service_instance, route: route, route_service_url: route_service_url },
924-
{ type: 'create', state: 'successful' }
1067+
{ type: 'create', state: 'successful' }
9251068
)
9261069
end
9271070
end

spec/support/space_restricted_response_generators.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,24 @@ def responses_for_space_restricted_single_endpoint(
2121
end
2222
end
2323
end
24+
25+
def responses_for_space_restricted_delete_endpoint(
26+
permitted_roles: SpaceRestrictedResponseGenerators.default_permitted_roles
27+
)
28+
Hash.new(code: 404).tap do |h|
29+
permitted_roles.each do |role|
30+
h[role] = { code: 204 }
31+
end
32+
end
33+
end
34+
35+
def responses_for_space_restricted_async_delete_endpoint(
36+
permitted_roles: SpaceRestrictedResponseGenerators.default_permitted_roles
37+
)
38+
Hash.new(code: 404).tap do |h|
39+
permitted_roles.each do |role|
40+
h[role] = { code: 202 }
41+
end
42+
end
43+
end
2444
end

0 commit comments

Comments
 (0)