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

Commit 1adf7a1

Browse files
author
Derik Evangelista
authored
v3(services): allow SI's to be purged
[finishes #171742693](https://www.pivotaltracker.com/story/show/171742693)
1 parent 8d85555 commit 1adf7a1

5 files changed

Lines changed: 242 additions & 16 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
module VCAP::CloudController
2+
class ServiceInstancePurge
3+
def initialize(event_repository)
4+
@event_repository = event_repository
5+
end
6+
7+
def purge(service_instance)
8+
logger.info("purging service instance #{service_instance.guid}")
9+
10+
service_instance.db.transaction do
11+
service_instance.routes.each do |route|
12+
route.route_binding.destroy
13+
end
14+
15+
service_instance.service_bindings.each do |binding|
16+
binding.destroy
17+
Repositories::ServiceBindingEventRepository.record_delete(binding, @event_repository.user_audit_info)
18+
end
19+
20+
service_instance.service_keys.each do |key|
21+
key.destroy
22+
@event_repository.record_service_key_event('delete', key, nil)
23+
end
24+
25+
service_instance.shared_spaces.each do |target_space|
26+
Repositories::ServiceInstanceShareEventRepository.record_unshare_event(service_instance, target_space, @event_repository.user_audit_info)
27+
end
28+
29+
service_instance.destroy
30+
@event_repository.record_service_instance_event('purge', service_instance, nil)
31+
end
32+
33+
logger.info("successfully purged service instance #{service_instance.guid}")
34+
end
35+
36+
def logger
37+
@logger ||= Steno.logger('cc.service_lifecycle.instance_purger')
38+
end
39+
end
40+
end

app/controllers/v3/service_instances_controller.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
require 'actions/service_instance_create_user_provided'
1919
require 'actions/v3/service_instance_delete'
2020
require 'actions/service_instance_create_managed'
21+
require 'actions/service_instance_purge'
2122
require 'fetchers/service_instance_list_fetcher'
2223
require 'decorators/field_service_instance_space_decorator'
2324
require 'decorators/field_service_instance_organization_decorator'
@@ -97,10 +98,17 @@ def update
9798
def destroy
9899
service_instance = ServiceInstance.first(guid: hashed_params[:guid])
99100
service_instance_not_found! unless service_instance && can_read_service_instance?(service_instance)
101+
purge = params['purge'] == 'true'
100102

101103
unauthorized! unless can_write_space?(service_instance.space)
102104

103105
service_event_repository = VCAP::CloudController::Repositories::ServiceEventRepository.new(user_audit_info)
106+
107+
if purge
108+
ServiceInstancePurge.new(service_event_repository).purge(service_instance)
109+
return [:no_content, nil]
110+
end
111+
104112
job_guid = V3::ServiceInstanceDelete.new(service_event_repository).delete(service_instance)
105113

106114
if job_guid.blank?

docs/v3/source/includes/experimental_resources/service_instances/_delete.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ accordingly to cases outlined in the [OSBAPI specification](https://github.com/o
4242
#### Definition
4343
`DELETE /v3/service_instances/:guid`
4444

45+
#### Query parameters
46+
47+
Name | Type | Description
48+
---- | ---- | ------------
49+
**purge** | _boolean_ | If `true`, deletes the service instance and all associated resources without any interaction with the service broker.
50+
4551
#### Permitted Roles
4652
|
4753
--- | ---

spec/request/service_instances_spec.rb

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2578,7 +2578,8 @@ def check_filtered_instances(*instances)
25782578
end
25792579

25802580
describe 'DELETE /v3/service_instances/:guid' do
2581-
let(:api_call) { lambda { |user_headers| delete "/v3/service_instances/#{instance.guid}", '{}', user_headers } }
2581+
let(:query_params) { '' }
2582+
let(:api_call) { lambda { |user_headers| delete "/v3/service_instances/#{instance.guid}?#{query_params}", '{}', user_headers } }
25822583

25832584
context 'permissions' do
25842585
let!(:instance) { VCAP::CloudController::UserProvidedServiceInstance.make(space: space) }
@@ -2602,7 +2603,9 @@ def check_filtered_instances(*instances)
26022603
end
26032604

26042605
context 'user provided service instances' do
2605-
let!(:instance) { VCAP::CloudController::UserProvidedServiceInstance.make(space: space) }
2606+
let!(:instance) { VCAP::CloudController::UserProvidedServiceInstance.make(space: space, route_service_url: 'https://banana.example.com/') }
2607+
let(:instance_labels) { VCAP::CloudController::ServiceInstanceLabelModel.where(service_instance: instance) }
2608+
let(:instance_annotations) { VCAP::CloudController::ServiceInstanceAnnotationModel.where(service_instance: instance) }
26062609

26072610
before do
26082611
VCAP::CloudController::ServiceInstanceLabelModel.make(key_name: 'fruit', value: 'banana', service_instance: instance)
@@ -2620,6 +2623,31 @@ def check_filtered_instances(*instances)
26202623
expect(VCAP::CloudController::ServiceInstanceLabelModel.where(service_instance: instance).all).to be_empty
26212624
expect(VCAP::CloudController::ServiceInstanceAnnotationModel.where(service_instance: instance).all).to be_empty
26222625
end
2626+
2627+
it 'fails to delete when there are bindings' do
2628+
VCAP::CloudController::ServiceBinding.make(service_instance: instance)
2629+
api_call.call(admin_headers)
2630+
expect(last_response).to have_status_code(422)
2631+
end
2632+
2633+
context 'with purge' do
2634+
let(:query_params) { 'purge=true' }
2635+
before(:each) do
2636+
@binding = VCAP::CloudController::ServiceBinding.make(service_instance: instance)
2637+
@route = VCAP::CloudController::RouteBinding.make(service_instance: instance)
2638+
end
2639+
2640+
it 'deletes the instance and the related resources' do
2641+
api_call.call(admin_headers)
2642+
expect(last_response).to have_status_code(204)
2643+
2644+
expect { instance.reload }.to raise_error Sequel::NoExistingObject
2645+
expect { @binding.reload }.to raise_error Sequel::NoExistingObject
2646+
expect { @route.reload }.to raise_error Sequel::NoExistingObject
2647+
expect(instance_labels.count).to eq(0)
2648+
expect(instance_annotations.count).to eq(0)
2649+
end
2650+
end
26232651
end
26242652

26252653
context 'managed service instance' do
@@ -3092,6 +3120,62 @@ def check_filtered_instances(*instances)
30923120
end
30933121
end
30943122

3123+
context 'when there are associations' do
3124+
RSpec.shared_examples 'associations not empty' do
3125+
it 'returns a 422 Unprocessable Entity' do
3126+
api_call.call(admin_headers)
3127+
expect(last_response).to have_status_code(422)
3128+
response = parsed_response['errors'].first
3129+
expect(response).to include('title' => 'CF-AssociationNotEmpty')
3130+
expect(response).to include('detail' => include('Please delete the service_bindings, service_keys, and routes associations for your service_instances.'))
3131+
end
3132+
end
3133+
3134+
let(:service_offering) { VCAP::CloudController::Service.make(requires: %w(route_forwarding)) }
3135+
let(:service_plan) { VCAP::CloudController::ServicePlan.make(service: service_offering) }
3136+
let(:instance) { VCAP::CloudController::ManagedServiceInstance.make(space: space, service_plan: service_plan) }
3137+
3138+
describe 'service bindings' do
3139+
before(:each) { VCAP::CloudController::ServiceBinding.make(service_instance: instance) }
3140+
it_should_behave_like 'associations not empty'
3141+
end
3142+
3143+
describe 'service keys' do
3144+
before(:each) { VCAP::CloudController::ServiceKey.make(service_instance: instance) }
3145+
it_should_behave_like 'associations not empty'
3146+
end
3147+
3148+
describe 'route bindings' do
3149+
before(:each) { VCAP::CloudController::RouteBinding.make(service_instance: instance) }
3150+
it_should_behave_like 'associations not empty'
3151+
end
3152+
3153+
context 'but purge is true' do
3154+
let(:query_params) { 'purge=true' }
3155+
before(:each) do
3156+
@binding = VCAP::CloudController::ServiceBinding.make(service_instance: instance)
3157+
@key = VCAP::CloudController::ServiceKey.make(service_instance: instance)
3158+
@route = VCAP::CloudController::RouteBinding.make(service_instance: instance)
3159+
3160+
api_call.call(admin_headers)
3161+
end
3162+
3163+
it 'removes all associations' do
3164+
expect { @binding.reload }.to raise_error Sequel::NoExistingObject
3165+
expect { @key.reload }.to raise_error Sequel::NoExistingObject
3166+
expect { @route.reload }.to raise_error Sequel::NoExistingObject
3167+
end
3168+
3169+
it 'deletes the service instance' do
3170+
expect { instance.reload }.to raise_error Sequel::NoExistingObject
3171+
end
3172+
3173+
it 'responds with 204' do
3174+
expect(last_response).to have_status_code(204)
3175+
end
3176+
end
3177+
end
3178+
30953179
context 'when the creation is still in progress' do
30963180
before do
30973181
instance.save_with_new_operation({}, {
@@ -3173,20 +3257,6 @@ def check_filtered_instances(*instances)
31733257
end
31743258
end
31753259

3176-
context 'when associations are not empty' do
3177-
let(:instance) { VCAP::CloudController::ServiceInstance.make(space: space) }
3178-
3179-
it 'returns a 422 Unprocessable Entity' do
3180-
VCAP::CloudController::ServiceBinding.make(service_instance: instance)
3181-
3182-
api_call.call(admin_headers)
3183-
expect(last_response).to have_status_code(422)
3184-
response = parsed_response['errors'].first
3185-
expect(response).to include('title' => 'CF-AssociationNotEmpty')
3186-
expect(response).to include('detail' => 'Please delete the service_bindings, service_keys, and routes associations for your service_instances.')
3187-
end
3188-
end
3189-
31903260
context 'when the service instance does not exist' do
31913261
let(:instance) { Struct.new(:guid).new('some-fake-guid') }
31923262
it 'returns a 404' do
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
require 'spec_helper'
2+
require 'actions/service_instance_purge'
3+
4+
module VCAP::CloudController
5+
RSpec.describe ServiceInstancePurge do
6+
let(:user_audit_info) { UserAuditInfo.new(user_guid: User.make.guid, user_email: 'email') }
7+
let(:event_repository) { Repositories::ServiceEventRepository.new(user_audit_info) }
8+
subject { described_class.new(event_repository) }
9+
10+
describe '#purge' do
11+
let(:service_instance) { ManagedServiceInstance.make }
12+
13+
it 'deletes the service instance' do
14+
subject.purge(service_instance)
15+
expect(service_instance).not_to exist
16+
end
17+
18+
it 'records a service instance purge event' do
19+
subject.purge(service_instance)
20+
event = Event.last
21+
expect(event.type).to eq('audit.service_instance.purge')
22+
expect(event.actee).to eq(service_instance.guid)
23+
end
24+
25+
it 'records a service usage event for DELETED' do
26+
subject.purge(service_instance)
27+
event = ServiceUsageEvent.last
28+
expect(event.service_instance_guid).to eq(service_instance.guid)
29+
expect(event.state).to eq('DELETED')
30+
end
31+
32+
context 'when there are service bindings' do
33+
let!(:service_binding_1) { ServiceBinding.make(service_instance: service_instance) }
34+
let!(:service_binding_2) { ServiceBinding.make(service_instance: service_instance) }
35+
36+
it 'records the service binding delete event' do
37+
subject.purge(service_instance)
38+
events = Event.where(type: 'audit.service_binding.delete').all
39+
event_binding_guids = events.collect(&:actee)
40+
41+
expect(events.length).to eq(2)
42+
expect(event_binding_guids).to match_array([service_binding_1.guid, service_binding_2.guid])
43+
end
44+
45+
it 'deletes the service bindings' do
46+
subject.purge(service_instance)
47+
expect(service_binding_1).not_to exist
48+
expect(service_binding_2).not_to exist
49+
end
50+
end
51+
52+
context 'when there are route bindings' do
53+
let(:route_1) { Route.make(space: service_instance.space) }
54+
let(:route_2) { Route.make(space: service_instance.space) }
55+
let!(:service_instance) { ManagedServiceInstance.make(:routing) }
56+
let!(:route_binding_1) { RouteBinding.make(service_instance: service_instance, route: route_1) }
57+
let!(:route_binding_2) { RouteBinding.make(service_instance: service_instance, route: route_2) }
58+
59+
it 'deletes the route bindings' do
60+
subject.purge(service_instance)
61+
expect(route_binding_1).not_to exist
62+
expect(route_binding_2).not_to exist
63+
end
64+
end
65+
66+
context 'when there are service keys' do
67+
let!(:service_key_1) { ServiceKey.make(service_instance: service_instance) }
68+
let!(:service_key_2) { ServiceKey.make(service_instance: service_instance) }
69+
70+
it 'records the service key delete event' do
71+
subject.purge(service_instance)
72+
events = Event.where(type: 'audit.service_key.delete').all
73+
event_key_guids = events.collect(&:actee)
74+
75+
expect(events.length).to eq(2)
76+
expect(event_key_guids).to match_array([service_key_1.guid, service_key_2.guid])
77+
end
78+
79+
it 'deletes the service keys' do
80+
subject.purge(service_instance)
81+
expect(service_key_1).not_to exist
82+
expect(service_key_2).not_to exist
83+
end
84+
end
85+
86+
context 'when the service instance has shared spaces' do
87+
let(:target_space) { Space.make }
88+
89+
before { service_instance.add_shared_space(target_space) }
90+
91+
it 'records an unshare service event' do
92+
subject.purge(service_instance)
93+
events = Event.where(type: 'audit.service_instance.unshare').all
94+
event_key_guid = events.collect(&:actee)
95+
96+
expect(events.length).to eq(1)
97+
expect(event_key_guid).to match_array([service_instance.guid])
98+
end
99+
end
100+
end
101+
end
102+
end

0 commit comments

Comments
 (0)