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

Commit ae8886f

Browse files
blgmFelisiaMDerik Evangelista
committed
V3: update managed service instance
Initial implementation for sync brokers. Async brokers to follow. [#171670120] Co-authored-by: Felisia Martini <fmartini@pivotal.io> Co-authored-by: Derik Evangelista <devangelista@pivotal.io> Co-authored-by: George Blue <gblue@pivotal.io>
1 parent 690b52b commit ae8886f

14 files changed

Lines changed: 1400 additions & 150 deletions

app/actions/service_instance_update.rb

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
require 'jobs/v3/update_service_instance_job'
2+
require 'repositories/service_event_repository'
3+
4+
module VCAP::CloudController
5+
class ServiceInstanceUpdateManaged
6+
class InvalidServiceInstance < StandardError
7+
end
8+
9+
class NameTakenForServiceInstance < CloudController::Errors::ApiError
10+
end
11+
12+
def initialize(service_event_repository)
13+
@service_event_repository = service_event_repository
14+
end
15+
16+
def update(service_instance, message)
17+
raise_if_name_already_taken!(service_instance, message)
18+
19+
begin
20+
lock = UpdaterLock.new(service_instance)
21+
lock.lock!
22+
23+
if update_broker_needed?(service_instance, message)
24+
job = update_async(service_instance, message)
25+
lock.asynchronous_unlock!
26+
return nil, job
27+
else
28+
si = update_sync(service_instance, message)
29+
lock.synchronous_unlock!
30+
return si, nil
31+
end
32+
ensure
33+
lock.unlock_and_fail! if lock.needs_unlock?
34+
end
35+
end
36+
37+
private
38+
39+
attr_reader :service_event_repository
40+
41+
def update_broker_needed?(service_instance, message)
42+
service_name_changed = message.requested?(:name) && service_instance.service.allow_context_updates
43+
parameters_changed = message.requested?(:parameters)
44+
service_plan_changed = message.service_plan_guid &&
45+
message.service_plan_guid != service_instance.service_plan.guid
46+
47+
service_name_changed || parameters_changed || service_plan_changed
48+
end
49+
50+
def update_sync(service_instance, message)
51+
logger = Steno.logger('cc.action.service_instance_update')
52+
53+
updates = {}
54+
updates[:name] = message.name if message.requested?(:name)
55+
updates[:tags] = message.tags if message.requested?(:tags)
56+
57+
service_instance.db.transaction do
58+
service_instance.update(updates) if updates.any?
59+
MetadataUpdate.update(service_instance, message)
60+
service_event_repository.record_service_instance_event(:update, service_instance, message.audit_hash)
61+
end
62+
63+
logger.info("Finished updating service_instance #{service_instance.guid}")
64+
return service_instance
65+
rescue Sequel::ValidationFailed => e
66+
raise InvalidServiceInstance.new(e.message)
67+
end
68+
69+
def update_async(service_instance, message)
70+
logger = Steno.logger('cc.action.service_instance_update')
71+
72+
update_job = V3::UpdateServiceInstanceJob.new(
73+
service_instance.guid,
74+
message: message,
75+
user_audit_info: service_event_repository.user_audit_info
76+
)
77+
pollable_job = Jobs::Enqueuer.new(update_job, queue: Jobs::Queues.generic).enqueue_pollable
78+
79+
logger.info("Queued job #{pollable_job.guid} to update service_instance #{service_instance.guid}")
80+
service_event_repository.record_service_instance_event(:start_update, service_instance, message.audit_hash)
81+
82+
return pollable_job
83+
end
84+
85+
def raise_if_name_already_taken!(service_instance, message)
86+
return unless message.requested?(:name)
87+
return unless service_instance.name != message.name
88+
return unless ServiceInstance.first(name: message.name, space: service_instance.space)
89+
90+
raise NameTakenForServiceInstance.new_from_details('ServiceInstanceNameTaken', message.name)
91+
end
92+
end
93+
end

app/actions/services/locks/updater_lock.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ def synchronous_unlock!
4343
@needs_unlock = false
4444
end
4545

46+
def asynchronous_unlock!
47+
@needs_unlock = false
48+
end
49+
4650
def enqueue_unlock!(job)
4751
enqueuer = Jobs::Enqueuer.new(job, queue: Jobs::Queues.generic)
4852
enqueuer.enqueue

app/controllers/v3/service_instances_controller.rb

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
require 'presenters/v3/service_instance_presenter'
1313
require 'actions/service_instance_share'
1414
require 'actions/service_instance_unshare'
15-
require 'actions/service_instance_update'
15+
require 'actions/service_instance_update_managed'
1616
require 'actions/service_instance_update_user_provided'
1717
require 'actions/service_instance_create_user_provided'
1818
require 'actions/service_instance_create_managed'
@@ -96,18 +96,9 @@ def update
9696

9797
case service_instance
9898
when ManagedServiceInstance
99-
message = ServiceInstanceUpdateManagedMessage.new(hashed_params[:body])
100-
unprocessable!(message.errors.full_messages) unless message.valid?
101-
102-
service_instance = ServiceInstanceUpdate.update(service_instance, message)
103-
render status: :ok, json: Presenters::V3::ServiceInstancePresenter.new(service_instance)
99+
update_managed(service_instance)
104100
when UserProvidedServiceInstance
105-
message = ServiceInstanceUpdateUserProvidedMessage.new(hashed_params[:body])
106-
unprocessable!(message.errors.full_messages) unless message.valid?
107-
108-
service_event_repository = VCAP::CloudController::Repositories::ServiceEventRepository::WithUserActor.new(user_audit_info)
109-
service_instance = ServiceInstanceUpdateUserProvided.new(service_event_repository).update(service_instance, message)
110-
render status: :ok, json: Presenters::V3::ServiceInstancePresenter.new(service_instance)
101+
update_user_provided(service_instance)
111102
end
112103
end
113104

@@ -187,7 +178,9 @@ def parameters
187178

188179
def create_user_provided(message)
189180
service_event_repository = VCAP::CloudController::Repositories::ServiceEventRepository::WithUserActor.new(user_audit_info)
181+
190182
instance = ServiceInstanceCreateUserProvided.new(service_event_repository).create(message)
183+
191184
render status: :created, json: Presenters::V3::ServiceInstancePresenter.new(instance)
192185
rescue ServiceInstanceCreateUserProvided::InvalidUserProvidedServiceInstance => e
193186
unprocessable!(e.message)
@@ -196,9 +189,7 @@ def create_user_provided(message)
196189
def create_managed(message, space:)
197190
service_event_repository = VCAP::CloudController::Repositories::ServiceEventRepository.new(user_audit_info)
198191
service_plan = ServicePlan.first(guid: message.service_plan_guid)
199-
unprocessable_service_plan! unless service_plan &&
200-
visible_to_current_user?(plan: service_plan) &&
201-
service_plan.visible_in_space?(space)
192+
unprocessable_service_plan! unless service_plan_valid?(service_plan, space)
202193

203194
broker_unavailable! unless service_plan.service_broker.available?
204195

@@ -209,6 +200,36 @@ def create_managed(message, space:)
209200
unprocessable!(e.message)
210201
end
211202

203+
def update_user_provided(service_instance)
204+
message = ServiceInstanceUpdateUserProvidedMessage.new(hashed_params[:body])
205+
unprocessable!(message.errors.full_messages) unless message.valid?
206+
207+
service_event_repository = VCAP::CloudController::Repositories::ServiceEventRepository::WithUserActor.new(user_audit_info)
208+
service_instance = ServiceInstanceUpdateUserProvided.new(service_event_repository).update(service_instance, message)
209+
render status: :ok, json: Presenters::V3::ServiceInstancePresenter.new(service_instance)
210+
end
211+
212+
def update_managed(service_instance)
213+
message = ServiceInstanceUpdateManagedMessage.new(hashed_params[:body])
214+
unprocessable!(message.errors.full_messages) unless message.valid?
215+
216+
if message.service_plan_guid
217+
service_plan = ServicePlan.first(guid: message.service_plan_guid)
218+
unprocessable_service_plan! unless service_plan_valid?(service_plan, service_instance.space)
219+
end
220+
221+
service_event_repository = VCAP::CloudController::Repositories::ServiceEventRepository.new(user_audit_info)
222+
service_instance, job = ServiceInstanceUpdateManaged.new(service_event_repository).update(service_instance, message)
223+
224+
if job.nil?
225+
render status: :ok, json: Presenters::V3::ServiceInstancePresenter.new(service_instance)
226+
else
227+
head :accepted, 'Location' => url_builder.build_url(path: "/v3/jobs/#{job.guid}")
228+
end
229+
rescue ServiceInstanceUpdateManaged::NameTakenForServiceInstance => api_err
230+
unprocessable!(api_err.message)
231+
end
232+
212233
def admin?
213234
permission_queryer.can_write_globally?
214235
end
@@ -282,6 +303,12 @@ def service_instance_not_found!
282303
resource_not_found!(:service_instance)
283304
end
284305

306+
def service_plan_valid?(service_plan, space)
307+
service_plan &&
308+
visible_to_current_user?(plan: service_plan) &&
309+
service_plan.visible_in_space?(space)
310+
end
311+
285312
def unprocessable_space!
286313
unprocessable!('Invalid space. Ensure that the space exists and you have access to it.')
287314
end
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# require 'messages/service_instance_update_managed_message'
2+
3+
module VCAP::CloudController
4+
module V3
5+
class UpdateServiceInstanceJob < VCAP::CloudController::Jobs::CCJob
6+
def initialize(service_instance_guid, message:, user_audit_info:)
7+
super()
8+
@service_instance_guid = service_instance_guid
9+
@message = message
10+
@user_audit_info = user_audit_info
11+
end
12+
13+
def perform
14+
logger = Steno.logger('cc.background')
15+
logger.info("Updating service instance #{service_instance_guid}")
16+
17+
gone! if service_instance.nil?
18+
19+
operation_in_progress = service_instance.last_operation.type
20+
aborted! if operation_in_progress != 'update'
21+
22+
client = VCAP::Services::ServiceClientProvider.provide({ instance: service_instance })
23+
broker_response, err = client.update(
24+
service_instance,
25+
service_plan,
26+
accepts_incomplete: false,
27+
arbitrary_parameters: message.parameters || {},
28+
previous_values: previous_values,
29+
name: message.requested?(:name) ? message.name : service_instance.name,
30+
)
31+
32+
if err
33+
service_instance.save_with_new_operation({}, broker_response[:last_operation])
34+
raise err
35+
end
36+
37+
updates = { service_plan: service_plan }
38+
updates['name'] = message.name if message.requested?(:name)
39+
updates['tags'] = message.tags if message.requested?(:tags)
40+
updates['dashboard_url'] = broker_response[:dashboard_url] if broker_response.key?(:dashboard_url)
41+
42+
ServiceInstance.db.transaction do
43+
service_instance.save_with_new_operation(updates, broker_response[:last_operation])
44+
MetadataUpdate.update(service_instance, message)
45+
record_event(service_instance, message.audit_hash)
46+
end
47+
48+
logger.info("Service instance update complete #{service_instance_guid}")
49+
end
50+
51+
def job_name_in_configuration
52+
:service_instance_update
53+
end
54+
55+
def max_attempts
56+
1
57+
end
58+
59+
def resource_type
60+
'service_instances'
61+
end
62+
63+
def resource_guid
64+
service_instance_guid
65+
end
66+
67+
def display_name
68+
'service_instance.update'
69+
end
70+
71+
private
72+
73+
attr_reader :service_instance_guid, :message, :user_audit_info
74+
75+
def service_instance
76+
ManagedServiceInstance.first(guid: service_instance_guid)
77+
end
78+
79+
def service_plan
80+
plan = if message.service_plan_guid
81+
ServicePlan.first(guid: message.service_plan_guid)
82+
else
83+
service_instance.service_plan
84+
end
85+
86+
service_plan_gone! unless plan
87+
plan
88+
end
89+
90+
def previous_values
91+
{
92+
plan_id: service_instance.service_plan.broker_provided_id,
93+
service_id: service_instance.service.broker_provided_id,
94+
organization_id: service_instance.organization.guid,
95+
space_id: service_instance.space.guid,
96+
}
97+
end
98+
99+
def record_event(service_instance, request_attrs)
100+
Repositories::ServiceEventRepository.new(@user_audit_info).
101+
record_service_instance_event(:update, service_instance, request_attrs)
102+
end
103+
104+
def service_plan_gone!
105+
raise CloudController::Errors::ApiError.new_from_details('ServicePlanNotFound', service_instance_guid)
106+
end
107+
108+
def gone!
109+
raise CloudController::Errors::ApiError.new_from_details('ServiceInstanceNotFound', service_instance_guid)
110+
end
111+
112+
def aborted!
113+
raise CloudController::Errors::ApiError.new_from_details('UnableToPerform', 'Update', 'delete in progress')
114+
end
115+
end
116+
end
117+
end
Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,53 @@
11
require 'messages/metadata_base_message'
2+
require 'messages/validators'
23

34
module VCAP::CloudController
45
class ServiceInstanceUpdateManagedMessage < MetadataBaseMessage
5-
register_allowed_keys []
6+
def self.relationships_requested?
7+
@relationships_requested ||= proc { |a| a.requested?(:relationships) }
8+
end
9+
10+
register_allowed_keys [
11+
:name,
12+
:tags,
13+
:parameters,
14+
:relationships,
15+
]
616

717
validates_with NoAdditionalKeysValidator
18+
validates_with RelationshipValidator, if: relationships_requested?
19+
20+
validates :name, string: true, allow_nil: true
21+
validates :tags, array: true, allow_blank: true
22+
validate :tags_must_be_strings
23+
24+
validates :parameters, hash: true, allow_nil: true
25+
validates :relationships, hash: true, allow_nil: true
26+
27+
def relationships_message
28+
@relationships_message ||= Relationships.new(relationships&.deep_symbolize_keys)
29+
end
30+
31+
delegate :service_plan_guid, to: :relationships_message
32+
33+
private
34+
35+
def tags_must_be_strings
36+
if tags.present? && tags.is_a?(Array) && tags.any? { |i| !i.is_a?(String) }
37+
errors.add(:tags, 'must be a list of strings')
38+
end
39+
end
40+
41+
class Relationships < BaseMessage
42+
register_allowed_keys [:service_plan]
43+
44+
validates_with NoAdditionalKeysValidator
45+
46+
validates :service_plan, presence: true, allow_nil: false, to_one_relationship: true
47+
48+
def service_plan_guid
49+
HashUtils.dig(service_plan, :data, :guid)
50+
end
51+
end
852
end
953
end

0 commit comments

Comments
 (0)