Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 35 additions & 14 deletions app/actions/service_instance_share.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,9 @@
module VCAP::CloudController
class ServiceInstanceShare
def create(service_instance, target_spaces, user_audit_info)
supported_service_type?(service_instance)
service_instance_shareable?(service_instance)

if target_spaces.include?(service_instance.space)
raise CloudController::Errors::ApiError.new_from_details('InvalidServiceInstanceSharingTargetSpace')
end

target_spaces.each do |space|
if space.service_instances.map(&:name).include?(service_instance.name)
raise CloudController::Errors::ApiError.new_from_details('SharedServiceInstanceNameTaken', service_instance.name, space.name)
end
end
supported_service_type!(service_instance)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These method names are a bit confusing. They read like boolean methods, but then have the danger bang.

Maybe something like

sharing_not_supported! unless supported_service_type?(service_instance)

or

validate_supported_service_type!(service_instance)

@deniseyu deniseyu Dec 13, 2017

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, we will rename it to validate_supported_service_type! since we throw different errors based on what type of service it is, and it feels a bit cleaner to leave this check inside the method.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes for this included in #1033 and can be viewed here: cloudfoundry-incubator@ff8276e#diff-e3c96f58c4686b95721452ad906b78a9R6

service_instance_shareable!(service_instance)
valid_target_spaces!(service_instance, target_spaces)

ServiceInstance.db.transaction do
target_spaces.each do |space|
Expand All @@ -30,7 +21,37 @@ def create(service_instance, target_spaces, user_audit_info)

private

def supported_service_type?(service_instance)
def valid_target_spaces!(service_instance, target_spaces)
no_sharing_to_self!(service_instance, target_spaces)

target_spaces.each do |space|
plan_visibility!(service_instance, space)
name_uniqueness!(service_instance, space)
end
end

def plan_visibility!(service_instance, space)
visible_plans = ServicePlan.organization_visible(space.organization)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be ServicePlan.space_visible(space) instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We investigated and learnt that these two methods are functionally the same here. space_visible adds an additional filter for space-scoped brokers, but these can never be shared because by definition the plan is only available in the source space -- which makes that filter a no-op. We'll change this to use space_visible for improved readability anyway and add some test cases to be more explicit about space-scoped brokers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes for this included in #1033 and can be viewed here: cloudfoundry-incubator@ff8276e#diff-e3c96f58c4686b95721452ad906b78a9R42


if !visible_plans.include?(service_instance.service_plan)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like we are violating encapsulation here: We are asking ServicePlan for all visible service plans and then checking a service plan if it is included in that list.

Could we instead encapsulate this logic in ServicePlan (or another object responsible for service plan visibility) and instead do something like

service_instance.service_plan.visible_in_space?(space)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, we've fixed this!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes for this included in #1033 and can be viewed here: cloudfoundry-incubator@ff8276e#diff-18a3dcea603c9a492aef637d3251c713R135

error_msg = "Access to service #{service_instance.service.label} and plan #{service_instance.service_plan.name} is not enabled in #{space.organization.name}/#{space.name}"
raise CloudController::Errors::ApiError.new_from_details('UnprocessableEntity', error_msg)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What distinguishes between errors that get custom error codes and ones that are just UnprocessableEntity?

A pattern that we have been using for our actions is to raise a custom error class with a message and then wrap them all as unprocessable at the controller layer:

def validation_error!(error)
if error.errors.on(:name)&.include?(:unique)
error!('Name must be unique')
end
error!(error.message)
end
def error!(message)
raise Error.new(message)
end

rescue OrganizationCreate::Error => e
unprocessable!(e.message)

Also, it looks like many of these errors are 400s. The api docs are a bit unclear, but in v3 we generally try to return 400s for syntactic errors and 422s for semantic errors (unfortunately, we are not always consistent with this.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I now see this PR exists: #1014

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Gerg. As this is a slightly bigger refactor, we will PR this separately. We created a story to track this: https://www.pivotaltracker.com/story/show/153639192

end
end

def name_uniqueness!(service_instance, space)
if space.service_instances.map(&:name).include?(service_instance.name)
raise CloudController::Errors::ApiError.new_from_details('SharedServiceInstanceNameTaken', service_instance.name, space.name)
end
end

def no_sharing_to_self!(service_instance, spaces)
if spaces.include?(service_instance.space)
raise CloudController::Errors::ApiError.new_from_details('InvalidServiceInstanceSharingTargetSpace')
end
end

def supported_service_type!(service_instance)
if service_instance.route_service?
raise CloudController::Errors::ApiError.new_from_details('RouteServiceInstanceSharingNotSupported')
end
Expand All @@ -40,7 +61,7 @@ def supported_service_type?(service_instance)
end
end

def service_instance_shareable?(service_instance)
def service_instance_shareable!(service_instance)
unless service_instance.shareable?
raise CloudController::Errors::ApiError.new_from_details('ServiceShareIsDisabled', service_instance.service.label)
end
Expand Down
55 changes: 55 additions & 0 deletions spec/unit/actions/service_instance_share_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,61 @@ module VCAP::CloudController
end
end
end

context 'when the service plan is private' do
let(:service_plan) { ServicePlan.make(public: false) }
let(:service_instance) { ManagedServiceInstance.make(service_plan: service_plan) }

it 'raises an api error' do
error_msg = "Access to service #{service_instance.service.label} and plan #{service_instance.service_plan.name} is not " \
"enabled in #{target_space1.organization.name}/#{target_space1.name}"
expect {
service_instance_share.create(service_instance, [target_space1], user_audit_info)
}.to raise_error(CloudController::Errors::ApiError, error_msg)
end

context 'and access has been enabled for the target org' do
before do
ServicePlanVisibility.make(organization: target_space1.organization, service_plan: service_instance.service_plan)
end

it 'creates the share' do
shared_instance = service_instance_share.create(service_instance, [target_space1], user_audit_info)
expect(shared_instance.shared_spaces.length).to eq 1
end
end

context 'and when the source org has service plan access disabled but the target org has service plan access enabled' do

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this different than 'and access has been enabled for the target org'?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, these two tests were doing the same thing. We have removed this test and added the missing "enabled in source + disabled in target" test.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes for this included in #1033 and can be viewed here: cloudfoundry-incubator@ff8276e#diff-a1a7ea72386627dabb82df75ec2e88dfR175

let(:source_org) { Organization.make }
let(:space) { Space.make(organization: source_org) }
let(:service_instance) { ManagedServiceInstance.make(service_plan: service_plan, space: space) }

before do
ServicePlanVisibility.make(organization: target_space1.organization, service_plan: service_instance.service_plan)
end

it 'creates the share' do
shared_instance = service_instance_share.create(service_instance, [target_space1], user_audit_info)
expect(shared_instance.shared_spaces.length).to eq 1
end
end

context 'and when source org has had service plan access enabled and the target org has service plan access enabled' do
let(:source_org) { Organization.make }
let(:space) { Space.make(organization: source_org) }
let(:service_instance) { ManagedServiceInstance.make(service_plan: service_plan, space: space) }

before do
ServicePlanVisibility.make(organization: target_space1.organization, service_plan: service_instance.service_plan)
ServicePlanVisibility.make(organization: source_org, service_plan: service_instance.service_plan)
end

it 'creates the share' do
shared_instance = service_instance_share.create(service_instance, [target_space1], user_audit_info)
expect(shared_instance.shared_spaces.length).to eq 1
end
end
end
end
end
end