Skip to content

Commit 79b4da3

Browse files
authored
Filter active service bindings per instance on dataset level (#4570)
- 'active' means latest in state 'create succeeded' (or without any operation) - filtering is done in SQL, not Ruby - no duplicate code where filtering is needed
1 parent b07c0f9 commit 79b4da3

File tree

6 files changed

+62
-43
lines changed

6 files changed

+62
-43
lines changed

app/models/runtime/helpers/service_operation_mixin.rb

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
module VCAP::CloudController
22
module ServiceOperationMixin
3+
CREATE = 'create'.freeze
4+
UPDATE = 'update'.freeze
5+
DELETE = 'delete'.freeze
6+
37
INITIAL = 'initial'.freeze
48
IN_PROGRESS = 'in progress'.freeze
59
SUCCEEDED = 'succeeded'.freeze
@@ -56,15 +60,15 @@ def last_operation?
5660
end
5761

5862
def create?
59-
last_operation&.type == 'create'
63+
last_operation&.type == CREATE
6064
end
6165

6266
def update?
63-
last_operation&.type == 'update'
67+
last_operation&.type == UPDATE
6468
end
6569

6670
def delete?
67-
last_operation&.type == 'delete'
71+
last_operation&.type == DELETE
6872
end
6973

7074
def initial?
@@ -83,4 +87,18 @@ def failed?
8387
last_operation&.state == FAILED
8488
end
8589
end
90+
91+
module ServiceOperationDatasetMixin
92+
def create_succeeded
93+
last_operation_table = Sequel[last_operation_association]
94+
95+
no_operation = Sequel.expr(last_operation_table[:id] => nil)
96+
create_and_succeeded = Sequel.expr(last_operation_table[:type] => ServiceOperationMixin::CREATE) &
97+
Sequel.expr(last_operation_table[:state] => ServiceOperationMixin::SUCCEEDED)
98+
99+
association_left_join(last_operation_association).
100+
where(no_operation | create_and_succeeded).
101+
select_all(first_source_alias)
102+
end
103+
end
86104
end

app/models/services/service_binding.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,5 +130,26 @@ def save_with_new_operation(last_operation, attributes: {})
130130
service_binding_operation(reload: true)
131131
end
132132
end
133+
134+
dataset_module ServiceOperationDatasetMixin
135+
136+
dataset_module do
137+
def last_operation_association
138+
:service_binding_operation
139+
end
140+
141+
# 'active' means latest in state 'create succeeded' (or without any operation)
142+
def active_per_instance
143+
create_succeeded.
144+
from_self.
145+
select_append do
146+
row_number.function.over(
147+
partition: :service_instance_guid,
148+
order: [Sequel.desc(:created_at), Sequel.desc(:id)]
149+
).as(:_rn)
150+
end.
151+
from_self.where(_rn: 1)
152+
end
153+
end
133154
end
134155
end

app/presenters/system_environment/system_env_presenter.rb

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
class SystemEnvPresenter
55
def initialize(app_or_process)
6+
@app_or_process = app_or_process
67
@service_binding_k8s_enabled = app_or_process.service_binding_k8s_enabled
78
@file_based_vcap_services_enabled = app_or_process.file_based_vcap_services_enabled
8-
@service_bindings = app_or_process.service_bindings
99
end
1010

1111
def system_env
@@ -22,14 +22,8 @@ def vcap_services
2222
private
2323

2424
def service_binding_env_variables
25-
latest_bindings = @service_bindings.
26-
select(&:create_succeeded?).
27-
group_by(&:service_instance_guid).
28-
values.
29-
map { |list| list.max_by(&:created_at) }
30-
3125
services_hash = {}
32-
latest_bindings.each do |service_binding|
26+
@app_or_process.service_bindings_dataset.active_per_instance.each do |service_binding|
3327
service_name = service_binding_label(service_binding)
3428
services_hash[service_name.to_sym] ||= []
3529
services_hash[service_name.to_sym] << service_binding_env_values(service_binding)

lib/cloud_controller/diego/service_binding_files_builder.rb

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ def initialize(app_or_process)
1313
@app_or_process = app_or_process
1414
@service_binding_k8s_enabled = app_or_process.service_binding_k8s_enabled
1515
@file_based_vcap_services = app_or_process.file_based_vcap_services_enabled
16-
@service_bindings = app_or_process.service_bindings
1716
end
1817

1918
def build
@@ -27,21 +26,14 @@ def build
2726

2827
private
2928

30-
# rubocop:disable Metrics/CyclomaticComplexity
3129
def build_service_binding_k8s
3230
return nil unless @service_binding_k8s_enabled
3331

3432
service_binding_files = {}
3533
names = Set.new # to check for duplicate binding names
3634
total_bytesize = 0 # to check the total bytesize
3735

38-
latest_bindings = @service_bindings.
39-
select(&:create_succeeded?).
40-
group_by(&:service_instance_guid).
41-
values.
42-
map { |list| list.max_by(&:created_at) }
43-
44-
latest_bindings.each do |service_binding|
36+
@app_or_process.service_bindings_dataset.active_per_instance.each do |service_binding|
4537
sb_hash = ServiceBindingPresenter.new(service_binding, include_instance: true).to_hash
4638
name = sb_hash[:name]
4739
raise IncompatibleBindings.new("Invalid binding name: '#{name}'. Name must match #{binding_naming_convention.inspect}") unless valid_binding_name?(name)
@@ -58,8 +50,6 @@ def build_service_binding_k8s
5850
total_bytesize += add_file(service_binding_files, name, 'type', label)
5951
total_bytesize += add_file(service_binding_files, name, 'provider', label)
6052
end
61-
# rubocop:enable Metrics/CyclomaticComplexity
62-
6353
raise IncompatibleBindings.new("Bindings exceed the maximum allowed bytesize of #{MAX_ALLOWED_BYTESIZE}: #{total_bytesize}") if total_bytesize > MAX_ALLOWED_BYTESIZE
6454

6555
service_binding_files.values

spec/unit/actions/service_credential_binding_app_create_spec.rb

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ module V3
4343

4444
describe '#precursor' do
4545
RSpec.shared_examples 'the credential binding precursor' do
46+
before { TestConfig.override(max_service_credential_bindings_per_app_service_instance: 1) }
47+
4648
it 'returns a service credential binding precursor' do
4749
binding = action.precursor(service_instance, app:, message:)
4850

@@ -69,9 +71,6 @@ module V3
6971
context 'when a binding already exists' do
7072
let!(:binding) { ServiceBinding.make(service_instance:, app:) }
7173

72-
# TODO: Once the unique constraints to allow multiple bindings are removed, this needs to be set to 1
73-
# before { TestConfig.override(max_service_credential_bindings_per_app_service_instance: 1) }
74-
7574
context 'when no last binding operation exists' do
7675
it 'raises an error' do
7776
expect { action.precursor(service_instance, app:, message:) }.to raise_error(
@@ -161,9 +160,6 @@ module V3
161160
end
162161
let(:service_instance2) { ManagedServiceInstance.make(**si_details) }
163162

164-
# TODO: Once the unique constraints to allow multiple bindings are removed, this needs to be set to 1
165-
# before { TestConfig.override(max_service_credential_bindings_per_app_service_instance: 1) }
166-
167163
it 'raises an error when the binding name already exists' do
168164
# First request, should succeed
169165
expect do
@@ -188,9 +184,6 @@ module V3
188184
)
189185
end
190186

191-
# TODO: Once the unique constraints to allow multiple bindings are removed, this needs to be set to 1
192-
# before { TestConfig.override(max_service_credential_bindings_per_app_service_instance: 1) }
193-
194187
it 'raises an error when the app is already bound to the service instance' do
195188
# First request, should succeed
196189
expect do
@@ -209,8 +202,6 @@ module V3
209202
let(:binding_1) { ServiceBinding.make(service_instance: service_instance, app: app, name: nil) }
210203

211204
before do
212-
# TODO: Once the unique constraints to allow multiple bindings are removed, this needs to be set to 1
213-
# TestConfig.override(max_service_credential_bindings_per_app_service_instance: 1)
214205
binding_1.save_with_attributes_and_new_operation({}, { type: 'create', state: 'succeeded' })
215206
end
216207

@@ -231,9 +222,6 @@ module V3
231222
context 'concurrent credential binding creation' do
232223
let(:name) { nil }
233224

234-
# TODO: Once the unique constraints to allow multiple bindings are removed, this needs to be set to 1
235-
# before { TestConfig.override(max_service_credential_bindings_per_app_service_instance: 1) }
236-
237225
it 'allows only one binding when two creates run in parallel' do
238226
# This test simulates a race condition for concurrent binding creation using a spy on `service_instance`.
239227
# We mock that a second binding is created after the first one acquires a lock and expect an `UnprocessableCreate` error.
@@ -251,11 +239,13 @@ module V3
251239
end
252240

253241
context 'when multiple bindings are allowed' do
242+
let(:binding_1) { ServiceBinding.make(service_instance:, app:, name:) }
243+
254244
before do
255245
# TODO: Remove skip when the service bindings unique constraints are removed
256246
skip 'this test can be enabled when the service bindings unique constraints are removed and max_bindings_per_app_service_instance can be configured'
257247

258-
binding_1 = ServiceBinding.make(service_instance:, app:, name:)
248+
TestConfig.override(max_service_credential_bindings_per_app_service_instance: 3)
259249
binding_2 = ServiceBinding.make(service_instance:, app:, name:)
260250
binding_1.save_with_attributes_and_new_operation({}, { type: 'create', state: 'succeeded' })
261251
binding_2.save_with_attributes_and_new_operation({}, { type: 'create', state: 'succeeded' })
@@ -276,8 +266,6 @@ module V3
276266
end
277267

278268
it 'raises an error if one of the bindings is in a failed state' do
279-
TestConfig.override(max_service_credential_bindings_per_app_service_instance: 2)
280-
281269
ServiceBinding.make(service_instance:, app:, name:).save_with_attributes_and_new_operation({}, { type: 'create', state: 'succeeded' })
282270
ServiceBinding.make(service_instance:, app:, name:).save_with_attributes_and_new_operation({}, { type: 'delete', state: 'failed' })
283271

@@ -304,8 +292,6 @@ module V3
304292
end
305293

306294
it 'raises an error if an existing binding has a different name' do
307-
TestConfig.override(max_service_credential_bindings_per_app_service_instance: 4)
308-
309295
ServiceBinding.make(service_instance: service_instance, app: app, name: 'other-name').
310296
save_with_attributes_and_new_operation({}, { type: 'create', state: 'succeeded' })
311297

spec/unit/lib/cloud_controller/diego/service_binding_files_builder_spec.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,19 @@ module VCAP::CloudController::Diego
3939
end
4040

4141
it 'uses the most recent binding' do
42-
expect(service_binding_files.find { |f| f.path == "#{directory}/binding-guid" }).to have_attributes(content: newer_binding.guid)
42+
expect(service_binding_files.find { |f| f.path == "#{directory}/binding_guid" }).to have_attributes(content: newer_binding.guid)
4343
expect(service_binding_files.find { |f| f.path == "#{directory}/name" }).to have_attributes(content: name || 'binding-name')
44-
expect(service_binding_files.find { |f| f.path == "#{directory}/binding-name" }).to have_attributes(content: 'binding-name') if name.nil?
44+
expect(service_binding_files.find { |f| f.path == "#{directory}/binding_name" }).to have_attributes(content: 'binding-name') if name.nil?
45+
end
46+
47+
context 'when the bindings have the same created_at timestamp' do
48+
let(:newer_binding_created_at) { binding_created_at }
49+
50+
it 'uses the most recent binding determined by highest id' do
51+
expect(service_binding_files.find { |f| f.path == "#{directory}/binding_guid" }).to have_attributes(content: newer_binding.guid)
52+
expect(service_binding_files.find { |f| f.path == "#{directory}/name" }).to have_attributes(content: name || 'binding-name')
53+
expect(service_binding_files.find { |f| f.path == "#{directory}/binding_name" }).to have_attributes(content: 'binding-name') if name.nil?
54+
end
4555
end
4656
end
4757
end

0 commit comments

Comments
 (0)