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

Commit 80e350f

Browse files
Derik Evangelistablgmbutzopower
authored
v3(services): deleting msi synchronously (cloudfoundry#1729)
[finishes #171726168](https://www.pivotaltracker.com/story/show/171726168) Co-authored-by: George Blue <gblue@pivotal.io> Co-authored-by: Brian Butz <butzopower@gmail.com>
1 parent ab03382 commit 80e350f

6 files changed

Lines changed: 388 additions & 53 deletions

File tree

app/actions/v3/service_instance_delete.rb

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
require 'jobs/v3/delete_service_instance_job'
2+
13
module VCAP::CloudController
24
module V3
35
class ServiceInstanceDelete
46
class AssociationNotEmptyError < StandardError; end
7+
58
class InstanceSharedError < StandardError; end
6-
class NotImplementedError < StandardError; end
79

810
def initialize(event_repo)
911
@service_event_repository = event_repo
@@ -14,20 +16,38 @@ def delete(service_instance)
1416

1517
cannot_delete_shared_instances! if service_instance.shared?
1618

17-
case service_instance
18-
when ManagedServiceInstance
19-
raise NotImplementedError
20-
end
19+
lock = DeleterLock.new(service_instance)
20+
lock.lock!
21+
22+
job = case service_instance
23+
when ManagedServiceInstance
24+
asynchronous_destroy(service_instance)
25+
when UserProvidedServiceInstance
26+
synchronous_destroy(service_instance, lock)
27+
end
2128

22-
service_instance.db.transaction do
23-
service_instance.lock!
24-
service_instance.destroy
25-
service_event_repository.record_user_provided_service_instance_event(:delete, service_instance, {})
26-
end
29+
job
2730
end
2831

2932
private
3033

34+
def synchronous_destroy(service_instance, lock)
35+
lock.unlock_and_destroy!
36+
service_event_repository.record_user_provided_service_instance_event(:delete, service_instance, {})
37+
nil
38+
end
39+
40+
def asynchronous_destroy(service_instance)
41+
delete_job = V3::DeleteServiceInstanceJob.new(
42+
service_instance.guid,
43+
:deprovision,
44+
service_event_repository.user_audit_info)
45+
46+
pollable_job = Jobs::Enqueuer.new(delete_job, queue: Jobs::Queues.generic).enqueue_pollable
47+
48+
pollable_job.guid
49+
end
50+
3151
def association_not_empty!
3252
raise AssociationNotEmptyError
3353
end

app/controllers/v3/service_instances_controller.rb

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,18 @@ def destroy
109109

110110
unauthorized! unless can_write_space?(service_instance.space)
111111

112-
service_event_repository = VCAP::CloudController::Repositories::ServiceEventRepository::WithUserActor.new(user_audit_info)
113-
V3::ServiceInstanceDelete.new(service_event_repository).delete(service_instance)
112+
service_event_repository = VCAP::CloudController::Repositories::ServiceEventRepository.new(user_audit_info)
113+
job_guid = V3::ServiceInstanceDelete.new(service_event_repository).delete(service_instance)
114114

115-
head :no_content
115+
if job_guid.blank?
116+
head :no_content
117+
else
118+
head :accepted, 'Location' => url_builder.build_url(path: "/v3/jobs/#{job_guid}")
119+
end
116120
rescue V3::ServiceInstanceDelete::AssociationNotEmptyError
117121
associations_not_empty!
118122
rescue V3::ServiceInstanceDelete::InstanceSharedError
119123
cannot_delete_shared_instances!(service_instance.name)
120-
rescue V3::ServiceInstanceDelete::NotImplementedError
121-
head :not_implemented
122124
end
123125

124126
def share_service_instance
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
require 'jobs/reoccurring_job'
2+
3+
module VCAP::CloudController
4+
module V3
5+
class DeleteServiceInstanceJob < VCAP::CloudController::Jobs::ReoccurringJob
6+
attr_reader :warnings
7+
8+
def initialize(guid, operation, audit_info)
9+
super()
10+
@service_instance_guid = guid
11+
@operation = operation
12+
@client_arguments = { accepts_incomplete: false }
13+
@user_audit_info = audit_info
14+
@warnings = []
15+
end
16+
17+
def perform
18+
client = VCAP::Services::ServiceClientProvider.provide({ instance: service_instance })
19+
20+
execute_request(client)
21+
22+
finish
23+
end
24+
25+
def job_name_in_configuration
26+
"service_instance_#{operation_type}"
27+
end
28+
29+
def max_attempts
30+
1
31+
end
32+
33+
def resource_type
34+
'service_instances'
35+
end
36+
37+
def resource_guid
38+
service_instance_guid
39+
end
40+
41+
def display_name
42+
"service_instance.#{operation_type}"
43+
end
44+
45+
private
46+
47+
attr_reader :service_instance_guid
48+
49+
def service_instance
50+
ManagedServiceInstance.first(guid: @service_instance_guid)
51+
end
52+
53+
def execute_request(client)
54+
broker_response = client.public_send(
55+
@operation,
56+
service_instance,
57+
@client_arguments
58+
)
59+
60+
if broker_response.dig(:last_operation, :state) == 'succeeded'
61+
ServiceInstance.db.transaction do
62+
si = service_instance
63+
service_instance.lock!
64+
service_instance.last_operation&.destroy
65+
service_instance.destroy
66+
record_event(si, nil)
67+
end
68+
end
69+
rescue => e
70+
service_instance.save_with_new_operation({}, {
71+
type: operation_type,
72+
state: 'failed',
73+
description: e.message,
74+
})
75+
raise e
76+
end
77+
78+
def record_event(service_instance, request_attrs)
79+
Repositories::ServiceEventRepository.new(@user_audit_info).
80+
record_service_instance_event(:delete, service_instance, request_attrs)
81+
end
82+
83+
def operation_type
84+
case @operation
85+
when :deprovision
86+
'delete'
87+
else
88+
''
89+
end
90+
end
91+
end
92+
end
93+
end

spec/request/service_instances_spec.rb

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2366,6 +2366,99 @@ def check_filtered_instances(*instances)
23662366

23672367
context 'managed service instance' do
23682368
let!(:instance) { VCAP::CloudController::ManagedServiceInstance.make(space: space) }
2369+
let(:broker_status_code) { 200 }
2370+
let(:broker_response) { {} }
2371+
2372+
before do
2373+
stub_request(:delete, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}").
2374+
with(query: {
2375+
# 'accepts_incomplete' => false,
2376+
'service_id' => instance.service.broker_provided_id,
2377+
'plan_id' => instance.service_plan.broker_provided_id
2378+
}).
2379+
to_return(status: broker_status_code, body: broker_response.to_json, headers: {})
2380+
end
2381+
2382+
it 'sets the service instance last operation to delete in progress' do
2383+
api_call.call(admin_headers)
2384+
expect(last_response).to have_status_code(HTTP::Status::ACCEPTED)
2385+
2386+
instance.reload
2387+
2388+
expect(instance.last_operation).to_not be_nil
2389+
expect(instance.last_operation.type).to eq('delete')
2390+
expect(instance.last_operation.state).to eq('in progress')
2391+
end
2392+
2393+
it 'responds with job resource' do
2394+
api_call.call(admin_headers)
2395+
expect(last_response).to have_status_code(202)
2396+
2397+
job = VCAP::CloudController::PollableJobModel.last
2398+
expect(last_response.headers['Location']).to end_with("/v3/jobs/#{job.guid}")
2399+
2400+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::PROCESSING_STATE)
2401+
expect(job.operation).to eq('service_instance.delete')
2402+
expect(job.resource_guid).to eq(instance.guid)
2403+
expect(job.resource_type).to eq('service_instances')
2404+
end
2405+
2406+
describe 'the pollable job' do
2407+
it 'sends a delete request with the right arguments to the service broker' do
2408+
api_call.call(admin_headers)
2409+
2410+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
2411+
2412+
expect(
2413+
a_request(:delete, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}").
2414+
with(query: {
2415+
'service_id' => instance.service.broker_provided_id,
2416+
'plan_id' => instance.service_plan.broker_provided_id
2417+
})
2418+
).to have_been_made.once
2419+
end
2420+
2421+
context 'when the service broker responds synchronously' do
2422+
context 'with success' do
2423+
it 'removes the service instance' do
2424+
api_call.call(admin_headers)
2425+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
2426+
2427+
expect(VCAP::CloudController::ServiceInstance.first(guid: instance.guid)).to be_nil
2428+
end
2429+
2430+
it 'completes the job' do
2431+
api_call.call(admin_headers)
2432+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
2433+
2434+
job = VCAP::CloudController::PollableJobModel.last
2435+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::COMPLETE_STATE)
2436+
end
2437+
end
2438+
2439+
context 'with an error' do
2440+
let(:broker_status_code) { 500 }
2441+
2442+
it 'marks the service instance as delete failed' do
2443+
api_call.call(admin_headers)
2444+
execute_all_jobs(expected_successes: 0, expected_failures: 1)
2445+
instance.reload
2446+
2447+
expect(instance.last_operation).to_not be_nil
2448+
expect(instance.last_operation.type).to eq('delete')
2449+
expect(instance.last_operation.state).to eq('failed')
2450+
end
2451+
2452+
it 'completes with failure' do
2453+
api_call.call(admin_headers)
2454+
execute_all_jobs(expected_successes: 0, expected_failures: 1)
2455+
2456+
job = VCAP::CloudController::PollableJobModel.last
2457+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)
2458+
end
2459+
end
2460+
end
2461+
end
23692462

23702463
context 'when it is shared' do
23712464
let(:other_space) { VCAP::CloudController::Space.make }

0 commit comments

Comments
 (0)