spec/unit/actions/app_apply_manifest_spec.rb
require 'spec_helper'
require 'actions/app_apply_manifest'
module VCAP::CloudController
RSpec.describe AppApplyManifest, job_context: :worker do
context 'when everything is mocked out' do
subject(:app_apply_manifest) { AppApplyManifest.new(user_audit_info) }
let(:user_audit_info) { instance_double(UserAuditInfo) }
let(:process_scale) { instance_double(ProcessScale) }
let(:route_mapping_delete) { instance_double(RouteMappingDelete) }
let(:app_update) { instance_double(AppUpdate) }
let(:app_patch_env) { instance_double(AppPatchEnvironmentVariables) }
let(:process_update) { instance_double(ProcessUpdate) }
let(:process_create) { instance_double(ProcessCreate) }
let(:service_cred_binding_create) { instance_double(V3::ServiceCredentialBindingAppCreate) }
let(:random_route_generator) { instance_double(RandomRouteGenerator, route: 'spiffy/donut') }
describe '#apply' do
before do
allow(RandomRouteGenerator).to receive(:new).and_return(random_route_generator)
allow(ProcessScale).
to receive(:new).and_return(process_scale)
allow(process_scale).to receive(:scale)
allow(ProcessCreate).
to receive(:new).and_return(process_create)
allow(process_create).to receive(:create)
allow(AppUpdate).
to receive(:new).and_return(app_update)
allow(app_update).to receive(:update)
allow(ProcessUpdate).
to receive(:new).and_return(process_update)
allow(process_update).to receive(:update)
allow(ManifestRouteUpdate).to receive(:update)
allow(SidecarUpdate).to receive(:update)
allow(SidecarCreate).to receive(:create)
allow(RouteMappingDelete).
to receive(:new).and_return(route_mapping_delete)
allow(route_mapping_delete).to receive(:delete)
allow(V3::ServiceCredentialBindingAppCreate).
to receive(:new).and_return(service_cred_binding_create)
allow(service_cred_binding_create).to receive(:precursor)
allow(service_cred_binding_create).to receive(:bind)
allow(AppPatchEnvironmentVariables).
to receive(:new).and_return(app_patch_env)
allow(app_patch_env).to receive(:patch)
end
describe 'scaling a process' do
describe 'scaling instances' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', instances: 4 }) }
let(:manifest_process_scale_message) { message.manifest_process_scale_messages.first }
let(:process) { ProcessModel.make(instances: 1) }
let(:app) { process.app }
context 'when the request is valid' do
it 'returns the app' do
expect(
app_apply_manifest.apply(app.guid, message)
).to eq(app)
end
it 'calls ProcessScale with the correct arguments' do
app_apply_manifest.apply(app.guid, message)
expect(ProcessScale).to have_received(:new).with(user_audit_info, process, an_instance_of(ProcessScaleMessage), manifest_triggered: true)
expect(process_scale).to have_received(:scale)
end
end
end
describe 'scaling memory' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', memory: '256MB' }) }
let(:manifest_process_scale_message) { message.manifest_process_scale_messages.first }
let(:process) { ProcessModel.make(memory: 512) }
let(:app) { process.app }
context 'when the request is valid' do
it 'returns the app' do
expect(
app_apply_manifest.apply(app.guid, message)
).to eq(app)
end
it 'calls ProcessScale with the correct arguments' do
app_apply_manifest.apply(app.guid, message)
expect(ProcessScale).to have_received(:new).with(user_audit_info, process, instance_of(ProcessScaleMessage), manifest_triggered: true)
expect(process_scale).to have_received(:scale)
end
end
end
end
describe 'updating buildpack' do
let(:buildpack) { VCAP::CloudController::Buildpack.make }
let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', buildpack: buildpack.name }) }
let(:app_update_message) { message.app_update_message }
let(:app) { AppModel.make }
context 'when the request is valid' do
it 'returns the app' do
expect(
app_apply_manifest.apply(app.guid, message)
).to eq(app)
end
it 'calls AppUpdate with the correct arguments' do
app_apply_manifest.apply(app.guid, message)
expect(AppUpdate).to have_received(:new).with(user_audit_info, manifest_triggered: true)
expect(app_update).to have_received(:update).
with(app, app_update_message, instance_of(AppBuildpackLifecycle))
end
end
context 'when the request is invalid due to failure to update the app' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', buildpack: buildpack.name }) }
before do
allow(app_update).
to receive(:update).and_raise(AppUpdate::InvalidApp.new('invalid app'))
end
it 'bubbles up the error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppUpdate::InvalidApp, 'invalid app')
end
end
end
describe 'updating stack' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'stack-test', stack: 'cflinuxfs4' }) }
let(:app_update_message) { message.app_update_message }
let(:app) { AppModel.make }
context 'when the request is valid' do
it 'returns the app' do
expect(
app_apply_manifest.apply(app.guid, message)
).to eq(app)
end
it 'calls AppUpdate with the correct arguments' do
app_apply_manifest.apply(app.guid, message)
expect(AppUpdate).to have_received(:new).with(user_audit_info, manifest_triggered: true)
expect(app_update).to have_received(:update).
with(app, app_update_message, instance_of(AppBuildpackLifecycle))
end
end
context 'when the request is invalid' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'stack-test', stack: 'no-such-stack' }) }
before do
allow(app_update).
to receive(:update).and_raise(AppUpdate::InvalidApp.new('invalid app'))
end
it 'bubbles up the error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppUpdate::InvalidApp, 'invalid app')
end
end
end
describe 'updating environment variables' do
let(:message) { AppManifestMessage.create_from_yml({ env: { foo: 'bar' } }) }
let(:app_update_environment_variables_message) { message.app_update_environment_variables_message }
let(:app) { AppModel.make }
context 'when the request is valid' do
it 'returns the app' do
expect(
app_apply_manifest.apply(app.guid, message)
).to eq(app)
end
it 'calls AppPatchEnvironmentVariables with the correct arguments' do
app_apply_manifest.apply(app.guid, message)
expect(AppPatchEnvironmentVariables).to have_received(:new).with(user_audit_info, manifest_triggered: true)
expect(app_patch_env).to have_received(:patch).
with(app, app_update_environment_variables_message)
end
end
context 'when the request is invalid' do
let(:message) { AppManifestMessage.create_from_yml({ env: 'not-a-hash' }) }
before do
allow(app_patch_env).
to receive(:patch).and_raise(AppPatchEnvironmentVariables::InvalidApp.new('invalid app'))
end
it 'bubbles up the error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppPatchEnvironmentVariables::InvalidApp, 'invalid app')
end
end
end
describe 'updating command' do
let(:message) { AppManifestMessage.create_from_yml({ command: 'new-command' }) }
let(:manifest_process_update_message) { message.manifest_process_update_messages.first }
let(:app) { AppModel.make }
context 'when the request is valid' do
it 'returns the app' do
expect(
app_apply_manifest.apply(app.guid, message)
).to eq(app)
end
it 'calls ProcessUpdate with the correct arguments' do
app_apply_manifest.apply(app.guid, message)
expect(ProcessUpdate).to have_received(:new).with(user_audit_info, manifest_triggered: true)
expect(process_update).to have_received(:update).
with(app.web_processes.first, manifest_process_update_message, ManifestStrategy)
end
end
context 'when the request is invalid' do
let(:message) { AppManifestMessage.create_from_yml({ command: '' }) }
before do
allow(process_update).
to receive(:update).and_raise(ProcessUpdate::InvalidProcess.new('invalid process'))
end
it 'bubbles up the error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(ProcessUpdate::InvalidProcess, 'invalid process')
end
end
end
describe 'updating multiple process attributes' do
let(:message) do
AppManifestMessage.create_from_yml({
processes: [
{ type: 'web', command: 'web-command', instances: 2 },
{ type: 'worker', command: 'worker-command', instances: 3 }
]
})
end
let!(:process1) { ProcessModel.make(type: 'web') }
let!(:app) { process1.app }
let!(:process2) { ProcessModel.make(app: app, type: 'worker') }
let(:manifest_process_update_message1) { message.manifest_process_update_messages.first }
let(:manifest_process_update_message2) { message.manifest_process_update_messages.last }
let(:manifest_process_scale_message1) { message.manifest_process_scale_messages.first }
let(:manifest_process_scale_message2) { message.manifest_process_scale_messages.last }
context 'when the request is valid' do
it 'returns the app' do
expect(
app_apply_manifest.apply(app.guid, message)
).to eq(app)
end
it 'calls ProcessUpdate with the correct arguments' do
app_apply_manifest.apply(app.guid, message)
expect(ProcessUpdate).to have_received(:new).with(user_audit_info, manifest_triggered: true).exactly(2).times
expect(process_update).to have_received(:update).with(process1, manifest_process_update_message1, ManifestStrategy)
expect(process_update).to have_received(:update).with(process2, manifest_process_update_message2, ManifestStrategy)
end
it 'calls ProcessScale with the correct arguments' do
app_apply_manifest.apply(app.guid, message)
expect(ProcessScale).to have_received(:new).with(user_audit_info, process1, instance_of(ProcessScaleMessage), manifest_triggered: true)
expect(ProcessScale).to have_received(:new).with(user_audit_info, process2, instance_of(ProcessScaleMessage), manifest_triggered: true)
expect(process_scale).to have_received(:scale).exactly(2).times
end
end
context 'when the request is invalid' do
let(:message) { AppManifestMessage.create_from_yml({ command: '' }) }
before do
allow(process_update).
to receive(:update).and_raise(ProcessUpdate::InvalidProcess.new('invalid process'))
end
it 'bubbles up the error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(ProcessUpdate::InvalidProcess, 'invalid process')
end
end
end
describe 'creating a new process' do
let(:message) do
AppManifestMessage.create_from_yml({
processes: [
{ type: 'potato', command: 'potato-command', instances: 3 }
]
})
end
let!(:app) { AppModel.make }
let(:update_message) { message.manifest_process_update_messages.first }
let(:scale_message) { message.manifest_process_scale_messages.first }
context 'when the request is valid' do
it 'returns the app' do
expect(
app_apply_manifest.apply(app.guid, message)
).to eq(app)
end
it 'calls ProcessCreate with command and type' do
app_apply_manifest.apply(app.guid, message)
expect(ProcessCreate).to have_received(:new).with(user_audit_info, manifest_triggered: true)
expect(process_create).to have_received(:create).with(app, { type: 'potato', command: 'potato-command' })
end
it 'updates and scales the newly created process with all the other properties' do
app_apply_manifest.apply(app.guid, message)
expect(ProcessUpdate).to have_received(:new).with(user_audit_info, manifest_triggered: true)
process = ProcessModel.last
expect(process_update).to have_received(:update).with(process, update_message, ManifestStrategy)
expect(ProcessScale).to have_received(:new).with(user_audit_info, process, instance_of(ProcessScaleMessage), manifest_triggered: true)
expect(process_scale).to have_received(:scale)
end
context 'when there is no command specified in the manifest' do
let(:message) do
AppManifestMessage.create_from_yml({
processes: [
{ type: 'potato', instances: 3 }
]
})
end
it 'sets the command to nil' do
app_apply_manifest.apply(app.guid, message)
expect(ProcessCreate).to have_received(:new).with(user_audit_info, manifest_triggered: true)
expect(process_create).to have_received(:create).with(app, { type: 'potato', command: nil })
end
end
end
end
describe 'updating health check type' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', health_check_type: 'process' }) }
let(:manifest_process_update_message) { message.manifest_process_update_messages.first }
let(:process) { ProcessModel.make }
let(:app) { process.app }
context 'when the request is invalid' do
let(:message) { AppManifestMessage.create_from_yml({ health_check_type: 'http' }) }
before do
allow(process_update).
to receive(:update).and_raise(ProcessUpdate::InvalidProcess.new('invalid process'))
end
it 'bubbles up the error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(ProcessUpdate::InvalidProcess, 'invalid process')
end
end
context 'when the request is valid' do
it 'returns the app' do
expect(
app_apply_manifest.apply(app.guid, message)
).to eq(app)
end
it 'calls ProcessUpdate with the correct arguments' do
app_apply_manifest.apply(app.guid, message)
expect(ProcessUpdate).to have_received(:new).with(user_audit_info, manifest_triggered: true)
expect(process_update).to have_received(:update).with(process, manifest_process_update_message, ManifestStrategy)
end
end
end
describe 'updating readiness health check type' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', readiness_health_check_type: 'port' }) }
let(:manifest_process_update_message) { message.manifest_process_update_messages.first }
let(:process) { ProcessModel.make }
let(:app) { process.app }
context 'when the request is invalid' do
let(:message) { AppManifestMessage.create_from_yml({ readiness_health_check_type: 'http' }) }
before do
allow(process_update).
to receive(:update).and_raise(ProcessUpdate::InvalidProcess.new('invalid process'))
end
it 'bubbles up the error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(ProcessUpdate::InvalidProcess, 'invalid process')
end
end
context 'when the request is valid' do
it 'returns the app' do
expect(
app_apply_manifest.apply(app.guid, message)
).to eq(app)
end
it 'calls ProcessUpdate with the correct arguments' do
app_apply_manifest.apply(app.guid, message)
expect(ProcessUpdate).to have_received(:new).with(user_audit_info, manifest_triggered: true)
expect(process_update).to have_received(:update).with(process, manifest_process_update_message, ManifestStrategy)
end
end
end
describe 'updating health check invocation_timeout' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', health_check_invocation_timeout: 47 }) }
let(:manifest_process_update_message) { message.manifest_process_update_messages.first }
let(:process) { ProcessModel.make }
let(:app) { process.app }
context 'when the request is valid' do
it 'returns the app' do
expect(
app_apply_manifest.apply(app.guid, message)
).to eq(app)
end
it 'calls ProcessUpdate with the correct arguments' do
app_apply_manifest.apply(app.guid, message)
expect(ProcessUpdate).to have_received(:new).with(user_audit_info, manifest_triggered: true)
expect(process_update).to have_received(:update).with(process, manifest_process_update_message, ManifestStrategy)
end
end
end
describe 'updating sidecars' do
let(:app) { AppModel.make }
let!(:sidecar) { SidecarModel.make(name: 'existing-sidecar', app: app) }
let(:message) do
AppManifestMessage.create_from_yml({ name: 'blah',
'sidecars' => [
{
'process_types' => ['web'],
'name' => 'new-sidecar',
'command' => 'rackup',
'memory' => '2G'
},
{
'process_types' => ['web'],
'name' => 'existing-sidecar',
'command' => 'rackup'
}
] })
end
it 'returns the app' do
expect(
app_apply_manifest.apply(app.guid, message)
).to eq(app)
expect(SidecarUpdate).to have_received(:update).with(sidecar, message.sidecar_create_messages.last)
expect(SidecarCreate).to have_received(:create).with(app.guid, message.sidecar_create_messages.first)
end
end
describe 'updating routes' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', routes: [{ route: 'http://tater.tots.com/tabasco' }] }) }
let(:manifest_routes_update_message) { message.manifest_routes_update_message }
let(:process) { ProcessModel.make }
let(:app) { process.app }
context 'when the request is valid' do
it 'returns the app' do
expect(
app_apply_manifest.apply(app.guid, message)
).to eq(app)
end
it 'calls ManifestRouteUpdate with the correct arguments' do
app_apply_manifest.apply(app.guid, message)
expect(ManifestRouteUpdate).to have_received(:update).with(app.guid, manifest_routes_update_message, user_audit_info)
end
end
end
describe 'updating with a random-route' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', random_route: true }) }
let(:manifest_routes_update_message) { message.manifest_routes_update_message }
let(:process) { ProcessModel.make }
let(:app) { process.app }
context 'when the app has no routes and the message specifies no routes' do
it 'provides a random route' do
app_apply_manifest.apply(app.guid, message)
expect(ManifestRouteUpdate).to have_received(:update) do |guid, msg, audit_info|
expect(guid).to eq(app.guid)
expect(msg.routes.first[:route]).to eq("#{app.name}-spiffy/donut.#{Domain.first.name}")
expect(audit_info).to eq(user_audit_info)
end
end
context 'when there is no shared domain' do
let(:domain) { PrivateDomain.make(owning_organization: app.organization) }
before do
Domain.dataset.destroy
domain # ensure domain is created after the dataset is truncated
end
it 'provides a random route within a domain scoped to the apps organization' do
app_apply_manifest.apply(app.guid, message)
expect(ManifestRouteUpdate).to have_received(:update) do |guid, msg, audit_info|
expect(guid).to eq(app.guid)
expect(msg.routes.first[:route]).to eq("#{app.name}-spiffy/donut.#{domain.name}")
expect(audit_info).to eq(user_audit_info)
end
end
end
context 'when there is no domains' do
before do
Domain.dataset.destroy
end
it 'fails with a NoDefaultDomain error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppApplyManifest::NoDefaultDomain, 'No default domains available')
end
end
end
context 'when the app has existing routes' do
let(:route1) { Route.make(space: app.space) }
let!(:route_mapping1) { RouteMappingModel.make(app: app, route: route1, process_type: process.type) }
it 'ignores the random_route' do
app_apply_manifest.apply(app.guid, message)
expect(ManifestRouteUpdate).not_to have_received(:update)
end
end
context 'when the message specifies routes' do
let(:message) do
AppManifestMessage.create_from_yml({ name: 'blah', random_route: true,
routes: [{ route: 'billy.tabasco.com' }] })
end
it 'ignores the random_route but uses the routes' do
app_apply_manifest.apply(app.guid, message)
expect(ManifestRouteUpdate).to have_received(:update).with(app.guid, manifest_routes_update_message, user_audit_info)
end
end
context 'when the message specifies an empty list of routes' do
let(:message) do
AppManifestMessage.create_from_yml({ name: 'blah', random_route: true,
routes: [] })
end
it 'ignores the random_route' do
app_apply_manifest.apply(app.guid, message)
expect(ManifestRouteUpdate).to have_received(:update).with(app.guid, manifest_routes_update_message, user_audit_info)
end
end
end
describe 'updating with a default-route' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', default_route: true }) }
let(:manifest_routes_update_message) { message.manifest_routes_update_message }
let(:process) { ProcessModel.make }
let(:app) { process.app }
context 'when the app has no routes and the message specifies no routes' do
it 'provides a default route' do
app_apply_manifest.apply(app.guid, message)
expect(ManifestRouteUpdate).to have_received(:update) do |guid, msg, audit_info|
expect(guid).to eq(app.guid)
expect(msg.routes.first[:route]).to eq("#{app.name}.#{Domain.first.name}")
expect(audit_info).to eq(user_audit_info)
end
end
context 'when the app name has special characters' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'blah!@#', default_route: true }) }
let(:app_model) { AppModel.make(name: 'blah!@#') }
it 'fails with a useful error message' do
expect do
app_apply_manifest.apply(app_model.guid, message)
end.to raise_error(AppApplyManifest::Error,
/Failed to create default route from app name: Host must be either "\*" or contain only alphanumeric characters, "_", or "-"/)
end
end
context 'when the app name is too long' do
let(:app_name) { 'a' * 100 }
let(:message) { AppManifestMessage.create_from_yml({ name: app_name, default_route: true }) }
let(:app_model) { AppModel.make(name: app_name) }
it 'fails with a useful error' do
expect do
app_apply_manifest.apply(app_model.guid, message)
end.to raise_error(AppApplyManifest::Error, 'Failed to create default route from app name: Host cannot exceed 63 characters')
end
end
context 'when there is no shared domain' do
let(:domain) { PrivateDomain.make(owning_organization: app.organization) }
before do
Domain.dataset.destroy
domain # ensure domain is created after the dataset is truncated
end
it 'provides a default route within a domain scoped to the apps organization' do
app_apply_manifest.apply(app.guid, message)
expect(ManifestRouteUpdate).to have_received(:update) do |guid, msg, audit_info|
expect(guid).to eq(app.guid)
expect(msg.routes.first[:route]).to eq("#{app.name}.#{domain.name}")
expect(audit_info).to eq(user_audit_info)
end
end
end
context 'when there is no domains' do
before do
Domain.dataset.destroy
end
it 'fails with a NoDefaultDomain error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppApplyManifest::NoDefaultDomain, 'No default domains available')
end
end
end
context 'when the app has existing routes' do
let(:route1) { Route.make(space: app.space) }
let!(:route_mapping1) { RouteMappingModel.make(app: app, route: route1, process_type: process.type) }
it 'ignores the default_route' do
app_apply_manifest.apply(app.guid, message)
expect(ManifestRouteUpdate).not_to have_received(:update)
end
end
context 'when the message specifies routes' do
let(:message) do
AppManifestMessage.create_from_yml({ name: 'blah', default_route: true,
routes: [{ route: 'billy.tabasco.com' }] })
end
it 'ignores the default_route but uses the routes' do
app_apply_manifest.apply(app.guid, message)
expect(ManifestRouteUpdate).to have_received(:update).with(app.guid, manifest_routes_update_message, user_audit_info)
end
end
context 'when the message specifies an empty list of routes' do
let(:message) do
AppManifestMessage.create_from_yml({ name: 'blah', default_route: true,
routes: [] })
end
it 'ignores the default_route' do
app_apply_manifest.apply(app.guid, message)
expect(ManifestRouteUpdate).to have_received(:update).with(app.guid, manifest_routes_update_message, user_audit_info)
end
end
end
describe 'deleting existing routes' do
let(:manifest_routes_update_message) { message.manifest_routes_update_message }
let(:process) { ProcessModel.make }
let(:app) { process.app }
let(:route1) { Route.make(space: app.space) }
let(:route2) { Route.make(space: app.space) }
let!(:route_mapping1) { RouteMappingModel.make(app: app, route: route1, process_type: process.type) }
let!(:route_mapping2) { RouteMappingModel.make(app: app, route: route2, process_type: process.type) }
context 'when no_route is true' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', no_route: true, random_route: true }) }
context 'when the request is valid' do
it 'returns the app' do
expect(
app_apply_manifest.apply(app.guid, message)
).to eq(app)
end
it 'calls RouteMappingDelete with the routes' do
app_apply_manifest.apply(app.guid, message)
expect(RouteMappingDelete).to have_received(:new).with(user_audit_info, manifest_triggered: true)
expect(route_mapping_delete).to have_received(:delete).with(array_including(route_mapping1, route_mapping2))
end
it 'does not generate a random route' do
app_apply_manifest.apply(app.guid, message)
expect(ManifestRouteUpdate).not_to have_received(:update)
end
end
end
context 'when no_route is false' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', no_route: false }) }
context 'when the request is valid' do
it 'returns the app' do
expect(
app_apply_manifest.apply(app.guid, message)
).to eq(app)
end
it 'does not call RouteMappingDelete' do
app_apply_manifest.apply(app.guid, message)
expect(route_mapping_delete).not_to have_received(:delete)
end
end
end
end
describe 'creating service bindings' do
let(:space) { Space.make }
let(:app) { AppModel.make(space:) }
before do
TestConfig.override(volume_services_enabled: false)
end
context 'valid request with list of services' do
let!(:service_instance) { ManagedServiceInstance.make(name: 'si-name', space: space) }
let!(:service_instance_2) { ManagedServiceInstance.make(name: 'si2-name', space: space) }
let(:binding_name) { Sham.name }
let(:message) do
AppManifestMessage.create_from_yml({ services: [service_instance.name,
{ 'name' => service_instance_2.name, parameters: { 'foo' => 'bar' }, binding_name: binding_name }] })
end
let(:service_binding_create_message_1) { instance_double(ServiceCredentialAppBindingCreateMessage) }
let(:service_binding_create_message_2) { instance_double(ServiceCredentialAppBindingCreateMessage) }
before do
allow(ServiceCredentialAppBindingCreateMessage).to receive(:new).and_return(service_binding_create_message_1, service_binding_create_message_2)
allow(service_cred_binding_create).to receive(:bind) { ServiceBinding.make }
allow_any_instance_of(ServiceBinding).to receive(:terminal_state?).and_return true
allow(service_cred_binding_create).to receive(:precursor).and_return(ServiceBinding.make)
allow(service_binding_create_message_1).to receive(:audit_hash).and_return({ foo: 'bar-1' })
allow(service_binding_create_message_1).to receive(:parameters)
allow(service_binding_create_message_2).to receive_messages(audit_hash: { foo: 'bar-2' }, parameters: { 'foo' => 'bar' })
end
it 'creates an action with the right arguments' do
app_apply_manifest.apply(app.guid, message)
expect(V3::ServiceCredentialBindingAppCreate).to have_received(:new).with(user_audit_info, { foo: 'bar-1' }, manifest_triggered: true)
expect(V3::ServiceCredentialBindingAppCreate).to have_received(:new).with(user_audit_info, { foo: 'bar-2' }, manifest_triggered: true)
end
it 'calls precursor with the correct arguments for each binding' do
app_apply_manifest.apply(app.guid, message)
expect(ServiceCredentialAppBindingCreateMessage).to have_received(:new).with(
type: AppApplyManifest::SERVICE_BINDING_TYPE,
name: nil,
parameters: {},
relationships: {
service_instance: {
data: {
guid: service_instance.guid
}
},
app: {
data: {
guid: app.guid
}
}
}
)
expect(ServiceCredentialAppBindingCreateMessage).to have_received(:new).with(
type: AppApplyManifest::SERVICE_BINDING_TYPE,
name: binding_name,
parameters: { foo: 'bar' },
relationships: {
service_instance: {
data: {
guid: service_instance_2.guid
}
},
app: {
data: {
guid: app.guid
}
}
}
)
expect(service_cred_binding_create).to have_received(:precursor).
with(service_instance, app: app, volume_mount_services_enabled: false, message: service_binding_create_message_1)
expect(service_cred_binding_create).to have_received(:precursor).
with(service_instance_2, app: app, volume_mount_services_enabled: false, message: service_binding_create_message_2)
end
it 'calls bind with the right arguments' do
service_binding_1 = instance_double(ServiceBinding)
service_binding_2 = instance_double(ServiceBinding)
allow(service_cred_binding_create).to receive(:precursor).and_return(
service_binding_1,
service_binding_2
)
app_apply_manifest.apply(app.guid, message)
expect(service_cred_binding_create).to have_received(:bind).with(service_binding_1, parameters: nil, accepts_incomplete: false)
expect(service_cred_binding_create).to have_received(:bind).with(service_binding_2, parameters: { 'foo' => 'bar' }, accepts_incomplete: false)
end
it 'wraps the error when precursor errors' do
allow(service_cred_binding_create).to receive(:precursor).and_raise('fake binding error')
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppApplyManifest::ServiceBindingError, /For service 'si-name': fake binding error/)
end
context 'service binding already exists' do
let(:message) { AppManifestMessage.create_from_yml({ services: [service_instance.name] }) }
let!(:binding) { ServiceBinding.make(service_instance:, app:) }
it 'does not create the binding' do
app_apply_manifest.apply(app.guid, message)
expect(service_cred_binding_create).not_to have_received(:bind)
end
context "last binding operation is 'create failed'" do
before do
binding.save_with_attributes_and_new_operation({}, { type: 'create', state: 'failed' })
end
it 'recreates the binding' do
allow(service_cred_binding_create).to receive(:precursor).and_return(binding)
app_apply_manifest.apply(app.guid, message)
expect(service_cred_binding_create).to have_received(:bind).with(binding, parameters: nil, accepts_incomplete: false)
end
end
end
context 'volume_services_enabled' do
let(:message) { AppManifestMessage.create_from_yml({ services: [service_instance.name] }) }
before do
TestConfig.override(volume_services_enabled: true)
end
it 'passes the volume_services_enabled_flag to ServiceBindingCreate' do
app_apply_manifest.apply(app.guid, message)
expect(service_cred_binding_create).to have_received(:precursor).
with(service_instance, app: app, volume_mount_services_enabled: true, message: service_binding_create_message_1)
end
end
context 'service binding errors' do
context 'bind happens async' do
context 'action starts async binding' do
before do
allow(service_cred_binding_create).to receive(:bind).with(anything, parameters: nil, accepts_incomplete: false).and_return({ async: true })
end
it 'raises an error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppApplyManifest::ServiceBindingError,
/For service 'si-name': The service broker responded asynchronously when a synchronous bind was requested./)
end
end
end
end
context 'service broker support sync or async bindings' do
context 'action prefers sync binding' do
let(:binding1) { ServiceBinding.make(service_instance:, app:) }
let(:binding2) { ServiceBinding.make(service_instance: service_instance_2, app: app) }
before do
allow_any_instance_of(ServiceBinding).to receive(:terminal_state?).and_call_original
allow_any_instance_of(AppApplyManifest).to receive(:sleep)
precursor_count = 0
allow(service_cred_binding_create).to receive(:precursor) do
precursor_count += 1
if precursor_count == 1
binding1
else
binding2
end
end
count = 0
allow(service_cred_binding_create).to receive(:bind).with(anything, accepts_incomplete: false) do
count += 1
if count == 1
ServiceBindingOperation.make(type: 'create', state: 'succeeded', service_binding: binding1)
binding1
else
ServiceBindingOperation.make(type: 'create', state: 'succeeded', service_binding: binding2)
binding2
end
end
end
it 'completes binding synchronously and does not try to poll' do
expect(service_cred_binding_create).to receive(:poll).exactly(0).times
app_apply_manifest.apply(app.guid, message)
end
end
end
context 'broker only supports async bindings' do
context 'action starts async binding' do
let(:binding1) { ServiceBinding.make(service_instance:, app:) }
let(:binding2) { ServiceBinding.make(service_instance: service_instance_2, app: app) }
before do
response = double(body: '{}', code: '422')
allow(service_cred_binding_create).to receive(:bind).
with(anything, parameters: anything, accepts_incomplete: false).and_raise VCAP::Services::ServiceBrokers::V2::Errors::AsyncRequired.new('fake message',
'POST', response)
allow_any_instance_of(ServiceBinding).to receive(:terminal_state?).and_call_original
allow_any_instance_of(AppApplyManifest).to receive(:sleep)
precursor_count = 0
allow(service_cred_binding_create).to receive(:precursor) do
precursor_count += 1
if precursor_count == 1
binding1
else
binding2
end
end
count = 0
allow(service_cred_binding_create).to receive(:bind).with(anything, parameters: anything, accepts_incomplete: true) do
count += 1
if count == 1
ServiceBindingOperation.make(type: 'create', state: 'initial', service_binding: binding1)
binding1
else
ServiceBindingOperation.make(type: 'create', state: 'initial', service_binding: binding2)
binding2
end
end
end
it 'polls service bindings until they are complete' do
allow(service_cred_binding_create).to receive(:poll).and_return(V3::ServiceBindingCreate::ContinuePolling.call(1.second),
V3::ServiceBindingCreate::ContinuePolling.call(1.second),
V3::ServiceBindingCreate::PollingFinished,
V3::ServiceBindingCreate::ContinuePolling.call(1.second),
V3::ServiceBindingCreate::PollingFinished)
expect(service_cred_binding_create).to receive(:poll).exactly(5).times
expect(app_apply_manifest).to receive(:sleep).with(1).exactly(3).times
app_apply_manifest.apply(app.guid, message)
end
it 'polls service bindings with the default sleep value' do
allow(service_cred_binding_create).to receive(:poll).and_return(V3::ServiceBindingCreate::ContinuePolling.call(nil),
V3::ServiceBindingCreate::ContinuePolling.call(nil),
V3::ServiceBindingCreate::PollingFinished,
V3::ServiceBindingCreate::ContinuePolling.call(nil),
V3::ServiceBindingCreate::PollingFinished)
expect(service_cred_binding_create).to receive(:poll).exactly(5).times
expect(app_apply_manifest).to receive(:sleep).with(5).exactly(3).times
app_apply_manifest.apply(app.guid, message)
end
it 'verifies exception is thrown if maximum polling duration is exceeded' do
TestConfig.override(max_manifest_service_binding_poll_duration_in_seconds: 15)
allow_any_instance_of(AppApplyManifest).to receive(:sleep) do |_action, seconds|
Timecop.travel(seconds.from_now)
end
allow(service_cred_binding_create).to receive(:poll).and_return(V3::ServiceBindingCreate::ContinuePolling.call(20.seconds),
V3::ServiceBindingCreate::ContinuePolling.call(20.seconds),
V3::ServiceBindingCreate::PollingFinished)
expect { app_apply_manifest.apply(app.guid, message) }.to raise_error(AppApplyManifest::ServiceBindingError)
expect(binding1.last_operation.state).to eq('failed')
expect(binding1.last_operation.description).to eq('Polling exceed the maximum polling duration')
orphan_mitigation_job = Delayed::Job.first
expect(orphan_mitigation_job).not_to be_nil
expect(orphan_mitigation_job).to be_a_fully_wrapped_job_of Jobs::Services::DeleteOrphanedBinding
end
it 'has a maximum retry_after' do
allow(service_cred_binding_create).to receive(:poll).and_return(V3::ServiceBindingCreate::ContinuePolling.call(24.hours),
V3::ServiceBindingCreate::PollingFinished)
expect(app_apply_manifest).to receive(:sleep).with(60)
app_apply_manifest.apply(app.guid, message)
end
context 'async binding fails' do
let(:binding) { ServiceBinding.make(service_instance:, app:) }
before do
allow(service_cred_binding_create).to receive(:precursor) { binding }
allow(service_cred_binding_create).to receive(:bind).with(anything, parameters: anything, accepts_incomplete: true) do
ServiceBindingOperation.make(type: 'create', state: 'initial', service_binding: binding)
binding
end
count = 0
allow(service_cred_binding_create).to receive(:poll) do
count += 1
raise V3::LastOperationFailedState unless count < 3
V3::ServiceBindingCreate::ContinuePolling.call(1.second)
end
end
it 'polls service bindings until they are in a terminal state' do
expect(service_cred_binding_create).to receive(:poll).exactly(3).times
expect(app_apply_manifest).to receive(:sleep).with(1).twice
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppApplyManifest::ServiceBindingError)
end
end
end
context 'bind fails with BindingNotRetrievable' do
before do
error = V3::ServiceBindingCreate::BindingNotRetrievable.new('The broker responded asynchronously but does not support fetching binding data.')
allow(service_cred_binding_create).to receive(:bind).and_raise(error)
end
it 'fails with async error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppApplyManifest::ServiceBindingError,
/For service 'si-name': The broker responded asynchronously but does not support fetching binding data./)
end
end
end
context 'service binding errors' do
context 'generic binding errors' do
before do
allow(service_cred_binding_create).to receive(:bind).and_raise('fake binding error')
end
it 'decorates the error with the name of the service instance' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppApplyManifest::ServiceBindingError, /For service 'si-name': fake binding error/)
end
end
context 'when a create is in progress for the same binding' do
let!(:binding) do
binding = ServiceBinding.make(service_instance:, app:)
binding.save_with_attributes_and_new_operation({}, { type: 'create', state: 'in progress' })
binding
end
it 'fails with a service binding error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppApplyManifest::ServiceBindingError, /For service 'si-name': A binding is being created. Retry this operation later./)
end
end
context 'when a delete is in progress for the same binding' do
let!(:binding) do
binding = ServiceBinding.make(service_instance:, app:)
binding.save_with_attributes_and_new_operation({}, { type: 'delete', state: 'in progress' })
binding
end
it 'fails with a service binding error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppApplyManifest::ServiceBindingError, /For service 'si-name': A binding is being deleted. Retry this operation later./)
end
end
context 'when a delete failed for the same binding' do
let!(:binding) do
binding = ServiceBinding.make(service_instance:, app:)
binding.save_with_attributes_and_new_operation({}, { type: 'delete', state: 'failed' })
binding
end
it 'fails with a service binding error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppApplyManifest::ServiceBindingError,
/For service 'si-name': A binding failed to be deleted. Resolve the issue with this binding before retrying this operation./)
end
end
end
context 'different service instance states' do
context 'when the last operation state is create in progress' do
before do
service_instance.save_with_new_operation({}, { type: 'create', state: 'in progress' })
allow(service_cred_binding_create).to receive(:precursor).and_raise(V3::ServiceCredentialBindingAppCreate::UnprocessableCreate,
'There is an operation in progress for the service instance')
end
it 'fails with a service binding error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppApplyManifest::ServiceBindingError,
"For service '#{service_instance.name}': There is an operation in progress for the service instance")
end
end
context 'when the last operation state is create succeeded' do
before do
service_instance.save_with_new_operation({}, { type: 'create', state: 'succeeded' })
end
it 'creates the binding' do
service_binding_1 = instance_double(ServiceBinding)
service_binding_2 = instance_double(ServiceBinding)
allow(service_cred_binding_create).to receive(:precursor).and_return(
service_binding_1,
service_binding_2
)
app_apply_manifest.apply(app.guid, message)
expect(service_cred_binding_create).to have_received(:bind).with(service_binding_1, parameters: nil, accepts_incomplete: false)
expect(service_cred_binding_create).to have_received(:bind).with(service_binding_2, parameters: { 'foo' => 'bar' }, accepts_incomplete: false)
end
end
context 'when the last operation state is create failed' do
before do
service_instance.save_with_new_operation({}, { type: 'create', state: 'failed' })
allow(service_cred_binding_create).to receive(:precursor).and_raise(V3::ServiceCredentialBindingAppCreate::UnprocessableCreate, 'Service instance not found')
end
it 'fails with a service binding error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppApplyManifest::ServiceBindingError,
"For service '#{service_instance.name}': Service instance not found")
end
end
context 'when the last operation state is update in progress' do
before do
service_instance.save_with_new_operation({}, { type: 'update', state: 'in progress' })
allow(service_cred_binding_create).to receive(:precursor).and_raise(V3::ServiceCredentialBindingAppCreate::UnprocessableCreate,
'There is an operation in progress for the service instance')
end
it 'fails with a service binding error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppApplyManifest::ServiceBindingError,
"For service '#{service_instance.name}': There is an operation in progress for the service instance")
end
end
context 'when the last operation state is update succeeded' do
before do
service_instance.save_with_new_operation({}, { type: 'update', state: 'succeeded' })
end
it 'creates the binding' do
service_binding_1 = instance_double(ServiceBinding)
service_binding_2 = instance_double(ServiceBinding)
allow(service_cred_binding_create).to receive(:precursor).and_return(
service_binding_1,
service_binding_2
)
app_apply_manifest.apply(app.guid, message)
expect(service_cred_binding_create).to have_received(:bind).with(service_binding_1, parameters: nil, accepts_incomplete: false)
expect(service_cred_binding_create).to have_received(:bind).with(service_binding_2, parameters: { 'foo' => 'bar' }, accepts_incomplete: false)
end
end
context 'when the last operation state is update failed' do
before do
service_instance.save_with_new_operation({}, { type: 'update', state: 'failed' })
end
it 'creates the binding' do
service_binding_1 = instance_double(ServiceBinding)
service_binding_2 = instance_double(ServiceBinding)
allow(service_cred_binding_create).to receive(:precursor).and_return(
service_binding_1,
service_binding_2
)
app_apply_manifest.apply(app.guid, message)
expect(service_cred_binding_create).to have_received(:bind).with(service_binding_1, parameters: nil, accepts_incomplete: false)
expect(service_cred_binding_create).to have_received(:bind).with(service_binding_2, parameters: { 'foo' => 'bar' }, accepts_incomplete: false)
end
end
context 'when the last operation state is delete in progress' do
before do
service_instance.save_with_new_operation({}, { type: 'delete', state: 'in progress' })
allow(service_cred_binding_create).to receive(:precursor).and_raise(V3::ServiceCredentialBindingAppCreate::UnprocessableCreate,
'There is an operation in progress for the service instance')
end
it 'fails with a service binding error' do
expect do
app_apply_manifest.apply(app.guid, message)
end.to raise_error(AppApplyManifest::ServiceBindingError,
"For service '#{service_instance.name}': There is an operation in progress for the service instance")
end
end
context 'when the last operation state is delete failed' do
before do
service_instance.save_with_new_operation({}, { type: 'delete', state: 'failed' })
end
it 'creates the binding' do
service_binding_1 = instance_double(ServiceBinding)
service_binding_2 = instance_double(ServiceBinding)
allow(service_cred_binding_create).to receive(:precursor).and_return(
service_binding_1,
service_binding_2
)
app_apply_manifest.apply(app.guid, message)
expect(service_cred_binding_create).to have_received(:bind).with(service_binding_1, parameters: nil, accepts_incomplete: false)
expect(service_cred_binding_create).to have_received(:bind).with(service_binding_2, parameters: { 'foo' => 'bar' }, accepts_incomplete: false)
end
end
end
end
end
describe 'when the app no longer exists' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', instances: 4 }) }
let(:app_guid) { 'fake-guid' }
it 'raises a NotFound error' do
expect do
app_apply_manifest.apply(app_guid, message)
end.to raise_error(CloudController::Errors::NotFound, "App with guid '#{app_guid}' not found")
end
end
end
end
context 'when we want to test manifest mechanisms' do
subject(:app_apply_manifest) { AppApplyManifest.new(user_audit_info) }
let(:user_audit_info) { UserAuditInfo.new(user_email: 'x@y.com', user_guid: 'hi guid') }
describe '#apply' do
context 'when changing memory' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', memory: '256MB' }) }
let(:process) { ProcessModel.make(memory: 512, state: ProcessModel::STARTED, type: 'web') }
let(:app) { process.app }
it "doesn't change the process's version" do
app.update(name: 'blah')
version = process.version
app_apply_manifest.apply(app.guid, message)
expect(process.reload.version).to eq(version)
expect(process.memory).to eq(256)
end
context 'when there are additional app web processes' do
let(:process2) { ProcessModel.make(memory: 513, state: ProcessModel::STARTED, type: 'web', app: app, created_at: process.created_at + 1) }
it 'operates on the most recent process for a given app' do
app.update(name: 'blah')
version = process.version
version2 = process2.version
app_apply_manifest.apply(app.guid, message)
process.reload
process2.reload
expect(process.version).to eq(version)
expect(process.memory).to eq(512)
expect(process2.version).to eq(version2)
expect(process2.memory).to eq(256)
end
end
end
end
end
end
end