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

Commit e5fcac4

Browse files
author
Derik Evangelista
authored
v3(bindings): create binding for MSI (sync) (cloudfoundry#1859)
[#174090447](https://www.pivotaltracker.com/story/show/174090447)
1 parent 356b3a5 commit e5fcac4

8 files changed

Lines changed: 448 additions & 44 deletions

app/actions/service_credential_binding_create.rb

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@ class UnprocessableCreate < StandardError
1010
class Unimplemented < StandardError
1111
end
1212

13-
def initialize(user_audit_info, volume_mount_services_enabled)
13+
def initialize(user_audit_info)
1414
@user_audit_info = user_audit_info
15-
@volume_mount_services_enabled = volume_mount_services_enabled
1615
end
1716

18-
def precursor(service_instance, app: nil, name: nil)
19-
validate!(service_instance, app)
17+
def precursor(service_instance, app: nil, name: nil, volume_mount_services_enabled: false)
18+
validate!(service_instance, app, volume_mount_services_enabled)
2019

2120
binding_details = {
2221
service_instance: service_instance,
@@ -39,13 +38,16 @@ def precursor(service_instance, app: nil, name: nil)
3938
raise UnprocessableCreate.new(e.full_message)
4039
end
4140

42-
def bind(binding)
41+
def bind(binding, parameters: {}, accepts_incomplete: false)
4342
client = VCAP::Services::ServiceClientProvider.provide(instance: binding.service_instance)
44-
details = client.bind(binding, arbitrary_parameters: {}, accepts_incomplete: false)
43+
details = client.bind(binding, arbitrary_parameters: parameters, accepts_incomplete: accepts_incomplete)
4544

46-
binding.save_with_new_operation({ type: 'create', state: 'succeeded' }, attributes: details[:binding])
47-
48-
event_repository.record_create(binding, @user_audit_info, manifest_triggered: false)
45+
if details[:async]
46+
save_incomplete_binding(binding, details[:operation])
47+
else
48+
binding.save_with_new_operation(operation_succeeded, attributes: details[:binding])
49+
event_repository.record_create(binding, @user_audit_info, manifest_triggered: false)
50+
end
4951
rescue => e
5052
binding.save_with_new_operation({
5153
type: 'create',
@@ -57,15 +59,26 @@ def bind(binding)
5759

5860
private
5961

60-
def validate!(service_instance, app)
62+
def operation_succeeded
63+
{ type: 'create', state: 'succeeded' }
64+
end
65+
66+
def save_incomplete_binding(binding, operation)
67+
binding.save_with_new_operation({
68+
type: 'create',
69+
state: 'in progress',
70+
broker_provided_operation: operation
71+
})
72+
end
73+
74+
def validate!(service_instance, app, volume_mount_services_enabled)
6175
app_is_required! unless app.present?
6276
space_mismatch! unless all_space_guids(service_instance).include? app.space.guid
6377

6478
if service_instance.managed_instance?
6579
service_not_bindable! unless service_instance.service_plan.bindable?
6680
service_not_available! unless service_instance.service_plan.active?
67-
volume_mount_not_enabled! if service_instance.volume_service? && !@volume_mount_services_enabled
68-
not_supported!
81+
volume_mount_not_enabled! if service_instance.volume_service? && !volume_mount_services_enabled
6982
end
7083
end
7184

app/controllers/v3/service_credential_bindings_controller.rb

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require 'messages/service_credential_binding_create_message'
1010
require 'decorators/include_binding_app_decorator'
1111
require 'decorators/include_binding_service_instance_decorator'
12+
require 'jobs/v3/create_service_credential_binding_job'
1213

1314
class ServiceCredentialBindingsController < ApplicationController
1415
def index
@@ -48,10 +49,17 @@ def create
4849
resource_not_accessible!('app', message.app_guid) unless can_access_resource?(app)
4950
unauthorized! unless can_write_to_space?(app.space)
5051

51-
action = V3::ServiceCredentialBindingCreate.new(user_audit_info, volume_services_enabled?)
52-
binding = action.precursor(service_instance, app: app, name: message.name)
53-
action.bind(binding)
54-
render status: :created, json: Presenters::V3::ServiceCredentialBindingPresenter.new(binding).to_hash
52+
action = V3::ServiceCredentialBindingCreate.new(user_audit_info)
53+
binding = action.precursor(service_instance, app: app, name: message.name, volume_mount_services_enabled: volume_services_enabled?)
54+
55+
case service_instance
56+
when ManagedServiceInstance
57+
pollable_job_guid = enqueue_bind_job(binding.guid, message.parameters)
58+
head :accepted, 'Location' => url_builder.build_url(path: "/v3/jobs/#{pollable_job_guid}")
59+
when UserProvidedServiceInstance
60+
action.bind(binding)
61+
render status: :created, json: Presenters::V3::ServiceCredentialBindingPresenter.new(binding).to_hash
62+
end
5563
rescue V3::ServiceCredentialBindingCreate::UnprocessableCreate => e
5664
unprocessable!(e.message)
5765
rescue V3::ServiceCredentialBindingCreate::Unimplemented
@@ -103,6 +111,16 @@ def parameters
103111

104112
private
105113

114+
def enqueue_bind_job(binding_guid, parameters)
115+
bind_job = VCAP::CloudController::V3::CreateServiceCredentialBindingJob.new(
116+
binding_guid,
117+
user_audit_info: user_audit_info,
118+
parameters: parameters
119+
)
120+
pollable_job = Jobs::Enqueuer.new(bind_job, queue: Jobs::Queues.generic).enqueue_pollable
121+
pollable_job.guid
122+
end
123+
106124
def resource_not_accessible!(resource, guid)
107125
unprocessable!("The #{resource} could not be found: '#{guid}'")
108126
end
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
require 'jobs/reoccurring_job'
2+
require 'actions/service_credential_binding_create'
3+
require 'cloud_controller/errors/api_error'
4+
5+
module VCAP::CloudController
6+
module V3
7+
class CreateServiceCredentialBindingJob < Jobs::ReoccurringJob
8+
def initialize(binding_guid, parameters:, user_audit_info:)
9+
super()
10+
@binding_guid = binding_guid
11+
@user_audit_info = user_audit_info
12+
@parameters = parameters
13+
@first_time = true
14+
end
15+
16+
def operation
17+
:bind
18+
end
19+
20+
def operation_type
21+
'create'
22+
end
23+
24+
def max_attempts
25+
1
26+
end
27+
28+
def display_name
29+
'service_bindings.create'
30+
end
31+
32+
def resource_guid
33+
@binding_guid
34+
end
35+
36+
def resource_type
37+
'service_binding'
38+
end
39+
40+
def perform
41+
binding = ServiceBinding.first(guid: @binding_guid)
42+
gone! unless binding
43+
44+
action = V3::ServiceCredentialBindingCreate.new(@user_audit_info)
45+
46+
if @first_time
47+
@first_time = false
48+
action.bind(binding, parameters: @parameters, accepts_incomplete: false)
49+
return finish if binding.reload.terminal_state?
50+
end
51+
52+
binding.save_with_new_operation({
53+
type: 'create',
54+
state: 'failed',
55+
description: 'async bindings are not supported'
56+
})
57+
finish
58+
rescue => e
59+
raise CloudController::Errors::ApiError.new_from_details('UnableToPerform', 'bind', e.message)
60+
end
61+
62+
private
63+
64+
def gone!
65+
raise CloudController::Errors::ApiError.new_from_details('ResourceNotFound', "The binding could not be found: #{@binding_guid}")
66+
end
67+
end
68+
end
69+
end

app/messages/service_credential_binding_create_message.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
module VCAP::CloudController
22
class ServiceCredentialBindingCreateMessage < BaseMessage
3-
register_allowed_keys [:type, :name, :relationships]
3+
register_allowed_keys [:type, :name, :relationships, :parameters]
44
validates_with NoAdditionalKeysValidator, RelationshipValidator
5+
validates :parameters, hash: true, allow_nil: true
56
validates :type, allow_blank: false, inclusion: {
67
in: %w(app),
78
message: "must be 'app'"

spec/request/service_credential_bindings_spec.rb

Lines changed: 126 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,7 @@ def check_filtered_bindings(*bindings)
798798

799799
describe 'POST /v3/service_credential_bindings' do
800800
let(:api_call) { ->(user_headers) { post '/v3/service_credential_bindings', create_body.to_json, user_headers } }
801-
801+
let(:request_extra) { {} }
802802
let(:app_to_bind_to) { VCAP::CloudController::AppModel.make(space: space) }
803803
let(:service_instance_details) {
804804
{
@@ -817,7 +817,7 @@ def check_filtered_bindings(*bindings)
817817
service_instance: { data: { guid: service_instance_guid } },
818818
app: { data: { guid: app_guid } }
819819
}
820-
}
820+
}.merge(request_extra)
821821
}
822822

823823
context 'user-provided service' do
@@ -879,9 +879,9 @@ def check_filtered_bindings(*bindings)
879879
get "/v3/service_credential_bindings/#{@binding_guid}/details", {}, admin_headers
880880
expect(last_response).to have_status_code(200)
881881
expect(parsed_response).to match_json_response({
882-
credentials: { password: 'foo' },
883-
syslog_drain_url: 'http://syslog.example.com/wow'
884-
})
882+
credentials: { password: 'foo' },
883+
syslog_drain_url: 'http://syslog.example.com/wow'
884+
})
885885
end
886886
end
887887
end
@@ -890,9 +890,128 @@ def check_filtered_bindings(*bindings)
890890
let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space: space) }
891891

892892
describe 'a successful creation' do
893-
it 'returns unimplemented' do
893+
let(:binding) { VCAP::CloudController::ServiceBinding.last }
894+
let(:job) { VCAP::CloudController::PollableJobModel.last }
895+
896+
it 'creates a credential binding in the database' do
897+
api_call.call(admin_headers)
898+
899+
expect(binding.service_instance).to eq(service_instance)
900+
expect(binding.app).to eq(app_to_bind_to)
901+
expect(binding.last_operation.state).to eq('in progress')
902+
expect(binding.last_operation.type).to eq('create')
903+
end
904+
905+
it 'responds with a job resource' do
894906
api_call.call(admin_headers)
895-
expect(last_response).to have_status_code(501)
907+
908+
expect(last_response).to have_status_code(202)
909+
expect(last_response.headers['Location']).to end_with("/v3/jobs/#{job.guid}")
910+
911+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::PROCESSING_STATE)
912+
expect(job.operation).to eq('service_bindings.create')
913+
expect(job.resource_guid).to eq(binding.guid)
914+
expect(job.resource_type).to eq('service_binding')
915+
916+
get "/v3/jobs/#{job.guid}", nil, admin_headers
917+
expect(last_response).to have_status_code(200)
918+
expect(parsed_response['guid']).to eq(job.guid)
919+
end
920+
921+
describe 'the pollable job' do
922+
let(:credentials) { { 'password' => 'special sauce' } }
923+
let(:broker_base_url) { service_instance.service_broker.broker_url }
924+
let(:broker_bind_url) { "#{broker_base_url}/v2/service_instances/#{service_instance.guid}/service_bindings/#{binding.guid}" }
925+
let(:broker_status_code) { 201 }
926+
let(:broker_response) { { credentials: credentials } }
927+
let(:client_body) do
928+
{
929+
context: {
930+
platform: 'cloudfoundry',
931+
organization_guid: org.guid,
932+
organization_name: org.name,
933+
space_guid: space.guid,
934+
space_name: space.name,
935+
},
936+
app_guid: app_to_bind_to.guid,
937+
service_id: service_instance.service_plan.service.unique_id,
938+
plan_id: service_instance.service_plan.unique_id,
939+
bind_resource: {
940+
app_guid: app_to_bind_to.guid,
941+
space_guid: service_instance.space.guid
942+
},
943+
}
944+
end
945+
946+
before do
947+
api_call.call(admin_headers)
948+
expect(last_response).to have_status_code(202)
949+
950+
stub_request(:put, broker_bind_url).
951+
to_return(status: broker_status_code, body: broker_response.to_json, headers: {})
952+
end
953+
954+
it 'sends a bind request with the right arguments to the service broker' do
955+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
956+
957+
expect(
958+
a_request(:put, broker_bind_url).
959+
with(
960+
body: client_body,
961+
)
962+
).to have_been_made.once
963+
end
964+
965+
context 'parameters are specified' do
966+
let(:request_extra) { { parameters: { foo: 'bar' } } }
967+
968+
it 'sends the parameters to the broker' do
969+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
970+
971+
expect(
972+
a_request(:put, broker_bind_url).
973+
with(
974+
body: client_body.deep_merge(request_extra)
975+
)
976+
).to have_been_made.once
977+
end
978+
end
979+
980+
context 'when the bind completes synchronously' do
981+
it 'updates the the binding' do
982+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
983+
984+
binding.reload
985+
expect(binding.credentials).to eq(credentials)
986+
expect(binding.last_operation.type).to eq('create')
987+
expect(binding.last_operation.state).to eq('succeeded')
988+
end
989+
990+
it 'completes the job' do
991+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
992+
993+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::COMPLETE_STATE)
994+
end
995+
end
996+
997+
context 'when the broker fails to bind' do
998+
let(:broker_status_code) { 422 }
999+
let(:broker_response) { { error: 'RequiresApp' } }
1000+
1001+
it 'updates the the binding with a failure' do
1002+
execute_all_jobs(expected_successes: 0, expected_failures: 1)
1003+
1004+
binding.reload
1005+
expect(binding.last_operation.type).to eq('create')
1006+
expect(binding.last_operation.state).to eq('failed')
1007+
end
1008+
1009+
it 'fails the job' do
1010+
execute_all_jobs(expected_successes: 0, expected_failures: 1)
1011+
1012+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)
1013+
end
1014+
end
8961015
end
8971016
end
8981017
end

0 commit comments

Comments
 (0)