Skip to content

Commit cf40b64

Browse files
authored
Lock app when creating service bindings (#4577)
A "FOR UPDATE" lock on the app instance together with checks implemented in Ruby ensure the same behavior as two unique constraints on database level, which need to be removed to support multiple bindings between an app and a service instance. The two checks are: - the number of bindings from an app to a service instance is below the configured limit - no other binding (to another service instance) for this app has the same name The validate_app_guid_name_uniqueness! method has been moved after the validation of already existing bindings - this simplifies its implementation. Method incomplete_deletion! raises a different (more precise) error when multiple bindings are active. Some unused methods have been removed. A test in spec/request/service_credential_bindings_spec.rb was not executed ('it' inside 'before'). Tests in spec/unit/actions/service_credential_binding_app_create_spec.rb have been reworked for consistence. Some unneeded (redundant) tests have been removed.
1 parent 8ecd426 commit cf40b64

File tree

4 files changed

+167
-231
lines changed

4 files changed

+167
-231
lines changed

app/actions/service_credential_binding_app_create.rb

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,14 @@ def precursor(service_instance, message:, app: nil, volume_mount_services_enable
3030

3131
ServiceBinding.new.tap do |new_binding|
3232
ServiceBinding.db.transaction do
33-
# Lock the service instance to prevent multiple bindings being created when not allowed
34-
service_instance.lock! if max_bindings_per_app_service_instance == 1
35-
36-
validate_app_guid_name_uniqueness!(app.guid, message.name, service_instance.guid)
33+
# Lock the app to ensure no other bindings are created concurrently and rules can be enforced
34+
# - number of bindings per service instance is below limit
35+
# - no other binding (to other service instance) for this app has the same name
36+
app.lock!
3737

3838
num_valid_bindings = 0
3939
existing_bindings = ServiceBinding.where(service_instance:, app:)
4040
existing_bindings.each do |binding|
41-
binding.lock!
42-
4341
if binding.create_failed?
4442
binding.destroy
4543
VCAP::Services::ServiceBrokers::V2::OrphanMitigator.new.cleanup_failed_bind(binding)
@@ -51,6 +49,7 @@ def precursor(service_instance, message:, app: nil, volume_mount_services_enable
5149
end
5250

5351
validate_number_of_bindings!(num_valid_bindings)
52+
validate_app_guid_name_uniqueness!(app.guid, message.name, service_instance.guid)
5453

5554
new_binding.save_with_attributes_and_new_operation(
5655
new_binding_details,
@@ -77,10 +76,9 @@ def validate_service_instance!(app, service_instance, volume_mount_services_enab
7776
end
7877

7978
def validate_binding!(binding, desired_binding_name:)
80-
already_bound! if (max_bindings_per_app_service_instance == 1) && (binding.create_succeeded? || binding.create_in_progress?)
79+
already_bound! if max_bindings_per_app_service_instance == 1 && (binding.create_succeeded? || binding.create_in_progress?)
8180
binding_in_progress!(binding.guid) if binding.create_in_progress?
8281
incomplete_deletion! if binding.delete_in_progress? || binding.delete_failed?
83-
8482
name_cannot_be_changed! if binding.name != desired_binding_name
8583
end
8684

@@ -91,10 +89,9 @@ def validate_number_of_bindings!(number_of_bindings)
9189
def validate_app_guid_name_uniqueness!(target_app_guid, desired_binding_name, target_service_instance_guid)
9290
return if desired_binding_name.nil?
9391

94-
dataset = ServiceBinding.where(app_guid: target_app_guid, name: desired_binding_name)
92+
return unless ServiceBinding.where(app_guid: target_app_guid, name: desired_binding_name).exclude(service_instance_guid: target_service_instance_guid).any?
9593

96-
name_uniqueness_violation!(desired_binding_name) if max_bindings_per_app_service_instance == 1 && dataset.first
97-
name_uniqueness_violation!(desired_binding_name) if dataset.exclude(service_instance_guid: target_service_instance_guid).first
94+
name_uniqueness_violation!(desired_binding_name)
9895
end
9996

10097
def permitted_binding_attributes
@@ -124,10 +121,6 @@ def app_is_required!
124121
raise UnprocessableCreate.new('No app was specified')
125122
end
126123

127-
def not_supported!
128-
raise Unimplemented.new('Cannot create credential bindings for managed service instances')
129-
end
130-
131124
def binding_in_progress!(binding_guid)
132125
raise UnprocessableCreate.new("There is already a binding in progress for this service instance and app (binding guid: #{binding_guid})")
133126
end
@@ -154,7 +147,9 @@ def already_bound!
154147
end
155148

156149
def incomplete_deletion!
157-
raise UnprocessableCreate.new('The binding is getting deleted or its deletion failed')
150+
raise UnprocessableCreate.new('The binding is getting deleted or its deletion failed') if max_bindings_per_app_service_instance == 1
151+
152+
raise UnprocessableCreate.new('A binding for this service instance and app is getting deleted or its deletion failed')
158153
end
159154

160155
def space_mismatch!
@@ -165,10 +160,6 @@ def service_not_bindable!
165160
raise UnprocessableCreate.new('Service plan does not allow bindings')
166161
end
167162

168-
def service_not_available!
169-
raise UnprocessableCreate.new('Service plan is not available')
170-
end
171-
172163
def volume_mount_not_enabled!
173164
raise UnprocessableCreate.new('Support for volume mount services is disabled')
174165
end

app/actions/service_credential_binding_key_create.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,6 @@ def service_not_bindable!
9292
raise UnprocessableCreate.new('Service plan does not allow bindings.')
9393
end
9494

95-
def service_not_available!
96-
raise UnprocessableCreate.new('Service plan is not available.')
97-
end
98-
9995
def volume_mount_not_enabled!
10096
raise UnprocessableCreate.new('Support for volume mount services is disabled.')
10197
end

spec/request/service_credential_bindings_spec.rb

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,20 +1275,20 @@ def check_filtered_bindings(*bindings)
12751275
end
12761276

12771277
context 'when only one binding per app and service instance is allowed' do
1278-
before do
1279-
it 'returns 422 when the binding already exists' do
1280-
api_call.call admin_headers
1281-
expect(last_response.status).to eq(201).or eq(202)
1278+
before { TestConfig.override(max_service_credential_bindings_per_app_service_instance: 1) }
12821279

1283-
api_call.call admin_headers
1280+
it 'returns 422 when the binding already exists' do
1281+
api_call.call admin_headers
1282+
expect(last_response.status).to eq(201).or eq(202)
12841283

1285-
expect(last_response).to have_status_code(422)
1286-
expect(parsed_response['errors']).to include(include({
1287-
'detail' => include('The app is already bound to the service instance'),
1288-
'title' => 'CF-UnprocessableEntity',
1289-
'code' => 10_008
1290-
}))
1291-
end
1284+
api_call.call admin_headers
1285+
1286+
expect(last_response).to have_status_code(422)
1287+
expect(parsed_response['errors']).to include(include({
1288+
'detail' => include('The app is already bound to the service instance'),
1289+
'title' => 'CF-UnprocessableEntity',
1290+
'code' => 10_008
1291+
}))
12921292
end
12931293
end
12941294

0 commit comments

Comments
 (0)