Skip to content

Commit

Permalink
v3(services) Filter service credential bindings by label (#2045)
Browse files Browse the repository at this point in the history
[#174261778](https://www.pivotaltracker.com/story/show/174261778)

* v3(services) Filter service credential bindings by label

Co-Authored-By: Marcela Campo <[email protected]>

* Fix formatting

Co-Authored-By: Marcela Campo <[email protected]>

* Added label selector docs

Co-Authored-By: Marcela Campo <[email protected]>

* Reuse ServiceCredBinding

Co-Authored-By: Marcela Campo <[email protected]>

Co-authored-by: Marcela Campo <[email protected]>
  • Loading branch information
FelisiaM and pivotal-marcela-campo authored Jan 12, 2021
1 parent 310446c commit 38be355
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 43 deletions.
10 changes: 10 additions & 0 deletions app/fetchers/service_credential_binding_list_fetcher.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'fetchers/base_list_fetcher'
require 'fetchers/label_selector_query_generator'

module VCAP
module CloudController
Expand Down Expand Up @@ -37,6 +38,15 @@ def filter(dataset, message)
dataset = dataset.where { f }
end

if message.requested?(:label_selector)
dataset = LabelSelectorQueryGenerator.add_selector_queries(
label_klass: ServiceCredentialBindingLabels::View,
resource_dataset: dataset,
requirements: message.requirements,
resource_klass: ServiceCredentialBinding::View
)
end

super(message, dataset, ServiceCredentialBinding::View)
end

Expand Down
4 changes: 2 additions & 2 deletions app/messages/service_credential_binding_list_message.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require 'messages/list_message'
require 'messages/metadata_list_message'

module VCAP::CloudController
class ServiceCredentialBindingListMessage < ListMessage
class ServiceCredentialBindingListMessage < MetadataListMessage
ARRAY_KEYS = [
:names,
:service_instance_guids,
Expand Down
36 changes: 36 additions & 0 deletions app/models/services/service_credential_binding_view.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,41 @@ class View < Sequel::Model(VIEW)
}
end
end

module ServiceCredentialBindingLabels
SERVICE_KEY_LABELS_VIEW = Sequel::Model(:service_key_labels).select(
Sequel.as(:service_key_labels__guid, :guid),
Sequel.as(:service_key_labels__resource_guid, :resource_guid),
Sequel.as(:service_key_labels__key_prefix, :key_prefix),
Sequel.as(:service_key_labels__key_name, :key_name),
Sequel.as(:service_key_labels__value, :value),
Sequel.as(ServiceCredentialBinding::Types::SERVICE_KEY, :type),
)

SERVICE_BINDING_LABELS_VIEW = Sequel::Model(:service_binding_labels).select(
Sequel.as(:service_binding_labels__guid, :guid),
Sequel.as(:service_binding_labels__resource_guid, :resource_guid),
Sequel.as(:service_binding_labels__key_prefix, :key_prefix),
Sequel.as(:service_binding_labels__key_name, :key_name),
Sequel.as(:service_binding_labels__value, :value),
Sequel.as(ServiceCredentialBinding::Types::SERVICE_BINDING, :type),
)

VIEW = [
SERVICE_KEY_LABELS_VIEW,
SERVICE_BINDING_LABELS_VIEW
].inject do |statement, sub_select|
statement.union(sub_select, all: true)
end.freeze

class View < Sequel::Model(VIEW)
plugin :single_table_inheritance,
:type,
model_map: {
'app' => 'VCAP::CloudController::ServiceBindingLabels',
'key' => 'VCAP::CloudController::ServiceKeyLabels'
}
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Name | Type | Description
**created_ats** (*experimental*)| _[timestamp](#timestamps)_ | Timestamp to filter by. When filtering on equality, several comma-delimited timestamps may be passed. Also supports filtering with [relational operators](#relational-operators-experimental)
**updated_ats** (*experimental*)| _[timestamp](#timestamps)_ | Timestamp to filter by. When filtering on equality, several comma-delimited timestamps may be passed. Also supports filtering with [relational operators](#relational-operators-experimental)
**include** | _list of strings_ | Optionally include a list of unique related resources in the response. Valid values are: `app`, `service_instance`
**label_selector** | _string_ | A query string containing a list of [label selector](#labels-and-selectors) requirements
**page** | _integer_ | Page to display; valid values are integers >= 1
**per_page** | _integer_ | Number of results per page; <br>valid values are 1 through 5000
**order_by** | _string_ | Value to sort by. Defaults to ascending; prepend with `-` to sort descending<br>Valid values are `created_at`, `updated_at`, and `name`
Expand Down
112 changes: 72 additions & 40 deletions spec/request/service_credential_bindings_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
guids: 'foo,bar',
created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}",
updated_ats: { gt: Time.now.utc.iso8601 },
label_selector: 'env'
}
end
end
Expand Down Expand Up @@ -165,10 +166,10 @@
it 'returns the filtered bindings' do
get "/v3/service_credential_bindings?order_by=created_at&service_instance_names=#{instance.name},#{other_instance.name}", nil, admin_headers
check_filtered_bindings(
expected_json(key_binding),
expected_json(other_key_binding),
expected_json(app_binding),
expected_json(other_app_binding)
key_binding,
other_key_binding,
app_binding,
other_app_binding
)
end
end
Expand All @@ -183,10 +184,10 @@
it 'returns the filtered bindings' do
get "/v3/service_credential_bindings?order_by=created_at&service_instance_guids=#{instance.guid},#{other_instance.guid}", nil, admin_headers
check_filtered_bindings(
expected_json(key_binding),
expected_json(other_key_binding),
expected_json(app_binding),
expected_json(other_app_binding)
key_binding,
other_key_binding,
app_binding,
other_app_binding
)
end
end
Expand All @@ -202,10 +203,10 @@
filter = "service_plan_names=#{instance.service_plan.name},#{other_instance.service_plan.name}"
get "/v3/service_credential_bindings?order_by=created_at&#{filter}", nil, admin_headers
check_filtered_bindings(
expected_json(key_binding),
expected_json(other_key_binding),
expected_json(app_binding),
expected_json(other_app_binding)
key_binding,
other_key_binding,
app_binding,
other_app_binding
)
end
end
Expand All @@ -221,10 +222,10 @@
filter = "service_plan_guids=#{instance.service_plan.guid},#{other_instance.service_plan.guid}"
get "/v3/service_credential_bindings?order_by=created_at&#{filter}", nil, admin_headers
check_filtered_bindings(
expected_json(key_binding),
expected_json(other_key_binding),
expected_json(app_binding),
expected_json(other_app_binding)
key_binding,
other_key_binding,
app_binding,
other_app_binding
)
end
end
Expand All @@ -240,10 +241,10 @@
filter = "service_offering_names=#{instance.service.name},#{other_instance.service.name}"
get "/v3/service_credential_bindings?order_by=created_at&#{filter}", nil, admin_headers
check_filtered_bindings(
expected_json(key_binding),
expected_json(other_key_binding),
expected_json(app_binding),
expected_json(other_app_binding)
key_binding,
other_key_binding,
app_binding,
other_app_binding
)
end
end
Expand All @@ -259,10 +260,10 @@
filter = "service_offering_guids=#{instance.service.guid},#{other_instance.service.guid}"
get "/v3/service_credential_bindings?order_by=created_at&#{filter}", nil, admin_headers
check_filtered_bindings(
expected_json(key_binding),
expected_json(other_key_binding),
expected_json(app_binding),
expected_json(other_app_binding)
key_binding,
other_key_binding,
app_binding,
other_app_binding
)
end
end
Expand All @@ -277,8 +278,8 @@
it 'returns the filtered bindings' do
get "/v3/service_credential_bindings?order_by=created_at&names=#{key_binding.name},#{other_app_binding.name}", nil, admin_headers
check_filtered_bindings(
expected_json(key_binding),
expected_json(other_app_binding)
key_binding,
other_app_binding
)
end
end
Expand All @@ -293,8 +294,8 @@
it 'returns the filtered bindings' do
get "/v3/service_credential_bindings?app_names=#{app_binding.app.name},#{other_app_binding.app.name}", nil, admin_headers
check_filtered_bindings(
expected_json(app_binding),
expected_json(other_app_binding)
app_binding,
other_app_binding
)
end
end
Expand All @@ -309,8 +310,8 @@
it 'returns the filtered bindings' do
get "/v3/service_credential_bindings?app_guids=#{app_binding.app.guid},#{other_app_binding.app.guid}", nil, admin_headers
check_filtered_bindings(
expected_json(app_binding),
expected_json(other_app_binding)
app_binding,
other_app_binding
)
end
end
Expand All @@ -337,26 +338,57 @@
it 'returns the filtered bindings' do
get '/v3/service_credential_bindings?type=key', nil, admin_headers
check_filtered_bindings(
expected_json(key_binding),
expected_json(other_key_binding),
expected_json(another_key)
key_binding,
other_key_binding,
another_key
)

get '/v3/service_credential_bindings?type=app', nil, admin_headers
check_filtered_bindings(
expected_json(app_binding),
expected_json(other_app_binding),
expected_json(another_binding)
app_binding,
other_app_binding,
another_binding
)
end
end

describe 'label_selector' do
let!(:key_labels) { { env: 'prod', animal: 'dog' } }
let!(:app_labels) { { env: 'prod', animal: 'horse' } }
before do
VCAP::CloudController::ServiceKeyLabelModel.make(key_name: 'fruit', value: 'strawberry', service_key: key_binding)
VCAP::CloudController::ServiceKeyLabelModel.make(key_name: 'env', value: 'prod', service_key: key_binding)
VCAP::CloudController::ServiceKeyLabelModel.make(key_name: 'animal', value: 'horse', service_key: key_binding)

VCAP::CloudController::ServiceKeyLabelModel.make(key_name: 'env', value: 'prod', service_key: other_key_binding)
VCAP::CloudController::ServiceKeyLabelModel.make(key_name: 'animal', value: 'dog', service_key: other_key_binding)

VCAP::CloudController::ServiceBindingLabelModel.make(key_name: 'env', value: 'prod', service_binding: app_binding)
VCAP::CloudController::ServiceBindingLabelModel.make(key_name: 'animal', value: 'horse', service_binding: app_binding)

VCAP::CloudController::ServiceBindingLabelModel.make(key_name: 'env', value: 'prod', service_binding: other_app_binding)

VCAP::CloudController::ServiceBindingLabelModel.make(key_name: 'env', value: 'staging', service_binding: another_binding)
VCAP::CloudController::ServiceBindingLabelModel.make(key_name: 'animal', value: 'dog', service_binding: another_binding)
end

it 'returns the filtered list' do
get '/v3/service_credential_bindings?label_selector=!fruit,env=prod,animal in (dog,horse)', nil, admin_headers

check_filtered_bindings(
other_key_binding,
app_binding,
)
end
end

def check_filtered_bindings(*bindings)
expect(last_response).to have_status_code(200)
expect(parsed_response['resources'].length).to be(bindings.length)
expect({ resources: parsed_response['resources'] }).to match_json_response(
{ resources: bindings }
)

returned_guids = parsed_response['resources'].map { |r| r['guid'] }
expected_guids = bindings.map(&:guid)
expect(returned_guids).to contain_exactly(*expected_guids)
end
end

Expand All @@ -365,7 +397,7 @@ def check_filtered_bindings(*bindings)
%w(
page per_page order_by created_ats updated_ats guids names service_instance_guids service_instance_names
service_plan_names service_plan_guids service_offering_names service_offering_guids app_guids app_names
include type
include type label_selector
)
end

Expand Down
21 changes: 21 additions & 0 deletions spec/unit/fetchers/service_credential_binding_list_fetcher_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'db_spec_helper'
require 'fetchers/service_credential_binding_list_fetcher'
require 'messages/service_credential_binding_list_message'
require 'fetchers/label_selector_query_generator'

module VCAP
module CloudController
Expand Down Expand Up @@ -143,6 +144,26 @@ module CloudController
end
end

context 'label selector' do
before do
ServiceKeyLabelModel.make(key_name: 'fruit', value: 'strawberry', service_key: key_binding)
ServiceKeyLabelModel.make(key_name: 'tier', value: 'backend', service_key: key_binding)

ServiceKeyLabelModel.make(key_name: 'fruit', value: 'lemon', service_key: another_key)

ServiceBindingLabelModel.make(key_name: 'tier', value: 'worker', service_binding: app_binding)

ServiceBindingLabelModel.make(key_name: 'fruit', value: 'strawberry', service_binding: another_binding)
ServiceBindingLabelModel.make(key_name: 'tier', value: 'worker', service_binding: another_binding)
end

let(:params) { { 'label_selector' => 'fruit=strawberry,tier in (backend,worker)' } }
it 'returns the right result' do
bindings = fetcher.fetch(space_guids: :all, message: message).all
expect(bindings.map(&:guid)).to contain_exactly(key_binding.guid, another_binding.guid)
end
end

context 'type' do
context 'app' do
let(:params) { { 'type' => 'app' } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ module VCAP::CloudController
'app_guids' => 'app-1-guid, app-2-guid, app-3-guid',
'app_names' => 'app-1-name, app-2-name, app-3-name',
'type' => 'app',
'include' => 'app,service_instance'
'include' => 'app,service_instance',
'label_selector' => 'key=value'
}
end

Expand Down Expand Up @@ -92,6 +93,16 @@ module VCAP::CloudController
expect(message).to be_valid
end
end

it 'validates metadata requirements' do
message = described_class.from_params({ 'label_selector' => '' }.with_indifferent_access)

expect_any_instance_of(Validators::LabelSelectorRequirementValidator).
to receive(:validate).
with(message).
and_call_original
message.valid?
end
end

describe 'order_by' do
Expand Down

0 comments on commit 38be355

Please sign in to comment.