fluke/devops-service/spec/executors/server_executor_spec.rb
2016-01-20 13:13:49 +03:00

819 lines
27 KiB
Ruby

require 'lib/executors/server_executor'
RSpec.describe Devops::Executor::ServerExecutor, type: :executor, stubbed_connector: true, stubbed_logger: true do
let(:project) { build(:project) }
let(:deploy_env) { project.deploy_env('foo') }
let(:server) { build(:server, project: project.id, deploy_env: 'foo') }
let(:output) { File.open(File::NULL, "w") }
let(:provider) { double('Provider instance') }
let(:executor) { described_class.new(server, output) }
before do
allow(stubbed_connector).to receive(:project) { project }
allow(executor.deploy_env).to receive(:provider_instance) { provider }
allow(server).to receive(:provider_instance) { provider }
end
describe '#initialize' do
it 'sets server, project, deploy_env, out instance variables' do
expect(executor.server).to eq server
expect(executor.deploy_env).to eq deploy_env
expect(executor).to have_instance_variable_value(:project, project)
expect(executor).to have_instance_variable_value(:out, output)
end
it 'defines :flush method on @out if it is absent' do
out = Class.new.new
expect(out).not_to respond_to(:flush)
described_class.new(server, out)
expect(out).to respond_to(:flush)
end
it 'sets current_user from options' do
user = double
executor = described_class.new(server, '', {current_user: user})
expect(executor).to have_instance_variable_value(:current_user, user)
end
end
describe '#report=' do
it 'sets report instance variable' do
executor.report= 'foo'
expect(executor).to have_instance_variable_value(:report, 'foo')
end
end
describe '#project=' do
it 'sets project instance variable' do
executor.project= 'foo'
expect(executor).to have_instance_variable_value(:project, 'foo')
end
end
describe '.symbolic_error_code' do
it 'returns symbol given an integer' do
expect(described_class.symbolic_error_code(2)).to eq :server_bootstrap_fail
end
it "returns :unknown_error if can't recognize error code" do
expect(described_class.symbolic_error_code(123)).to eq :unknown_error
end
end
describe '#create_server_object' do
it 'builds Server object' do
server = executor.create_server_object('created_by' => 'me')
expect(server).to be_a(Devops::Model::Server)
expect(server.project).to eq 'my_project'
expect(server.deploy_env).to eq 'foo'
expect(server.created_by).to eq 'me'
end
end
describe '#create_server' do
let(:image) { double('Image instance', remote_user: 'remote_user') }
let(:create_server) {
executor.create_server(
'created_by' => 'user',
'run_list' => @run_list,
'name' => 'node_name',
'key' => @key,
'without_bootstrap' => @without_bootstrap
)
}
before do
allow(provider).to receive(:create_server) { true }
allow(stubbed_connector).to receive(:image) { image }
allow(stubbed_connector).to receive(:server_insert)
@without_bootstrap = true
@run_list = %w(role[asd])
@key = 'key'
end
it 'builds server model from given options' do
create_server
expect(executor.server.created_by).to eq 'user'
expect(executor.server.chef_node_name).to eq 'node_name'
expect(executor.server.key).to eq @key
expect(executor.server.run_list).to eq @run_list
end
it 'sets run list to an empty array by default' do
@run_list = nil
create_server
expect(executor.server.run_list).to eq []
end
it 'sets key to default provider ssh key by default' do
@key = nil
allow(provider).to receive(:ssh_key) { 'default_key' }
create_server
expect(executor.server.key).to eq 'default_key'
end
it 'runs hooks' do
expect(executor).to receive(:run_hook).with(:before_create).ordered
expect(executor).to receive(:run_hook).with(:after_create).ordered
create_server
end
it 'creates server in cloud' do
expect(provider).to receive(:create_server).with(
an_instance_of(Devops::Model::Server), deploy_env.image, deploy_env.flavor, deploy_env.subnets, deploy_env.groups, output
)
create_server
end
it 'inserts built server into mongo' do
expect(stubbed_connector).to receive(:server_insert)
create_server
end
it 'schedules expiration for server' do
deploy_env.expires = '2m'
allow(DeleteServerWorker).to receive(:perform_in)
expect(DeleteServerWorker).to receive(:perform_in).with(120, {server_chef_node_name: 'node_name'})
create_server
end
it "doesn't schedule expiration if deploy_env.expires is nil" do
deploy_env.expires = nil
expect(DeleteServerWorker).not_to receive(:perform_in)
create_server
end
context 'without_bootstrap option is false' do
it 'launches bootstrap' do
@without_bootstrap = false
allow(image).to receive(:bootstrap_template) { 'template' }
allow(executor).to receive(:two_phase_bootstrap)
expect(executor).to receive(:two_phase_bootstrap)
create_server
end
end
context 'without_bootstrap option is nil' do
it 'launches bootstrap' do
@without_bootstrap = nil
allow(image).to receive(:bootstrap_template) { 'template' }
allow(executor).to receive(:two_phase_bootstrap)
expect(executor).to receive(:two_phase_bootstrap)
create_server
end
end
context 'without_bootstrap option is true' do
it "doesn't launch bootstrap" do
@without_bootstrap = true
expect(executor).not_to receive(:two_phase_bootstrap)
create_server
end
end
context 'if error has been raised during execution' do
before do
allow(stubbed_connector).to receive(:server_delete)
allow(provider).to receive(:create_server) { raise }
end
it 'rollbacks server creating' do
expect(executor).to receive(:roll_back)
create_server
end
it 'deletes server from mongo' do
expect(stubbed_connector).to receive(:server_delete)
create_server
end
end
context "if creating server in cloud wasn't successful" do
it 'returns creating_server_in_cloud_failed error code' do
allow(provider).to receive(:create_server) { false }
expect(create_server).to eq 10
end
end
end
describe '#bootstrap', stubbed_knife: true do
let(:image) { double('Key instance', path: 'path') }
let(:bootstrap) { executor.bootstrap({}) }
before do
allow(executor).to receive(:sleep)
allow(executor).to receive(:last_command_successful?).and_return(true)
allow(executor).to receive(:execute_system_command)
allow(provider).to receive(:create_default_chef_node_name).and_return('chef_node')
allow(stubbed_connector).to receive(:key).and_return(image)
allow(stubbed_connector).to receive(:server_set_chef_node_name)
allow(stubbed_knife).to receive(:knife_bootstrap).and_return(0)
end
it 'run before hook' do
expect(executor).to receive(:run_hook).with(:before_bootstrap, output).ordered
expect(executor).to receive(:run_hook).with(:after_bootstrap, output).ordered
bootstrap
end
context "when server's private ip is unset" do
it 'returns server_bootstrap_private_ip_unset error code' do
server.private_ip = nil
expect(bootstrap).to eq 4
end
end
it 'tries to ssh to server' do
expect(executor).to receive(:execute_system_command).with(/ssh/)
bootstrap
end
context "couldn't ssh to server" do
before { allow(executor).to receive(:last_command_successful?) { false } }
it 'tries to ssh to server maximum MAX_SSH_RETRIES_AMOUNT times' do
max_retries = Devops::Executor::ServerExecutor::MAX_SSH_RETRIES_AMOUNT
expect(executor).to receive(:execute_system_command).exactly(max_retries).times
bootstrap
end
it 'returns server_bootstrap_fail error code' do
expect(bootstrap).to eq 2
end
end
context 'after successful ssh check' do
before { allow(executor).to receive(:last_command_successful?).and_return(false, true) }
it "sets default chef node name if it's nil" do
executor.server.chef_node_name = nil
expect {bootstrap}.to change {executor.server.chef_node_name}.to 'chef_node'
end
it 'executes knife bootstrap' do
expect(stubbed_knife).to receive(:knife_bootstrap).with(output, server.private_ip, instance_of(Array))
bootstrap
end
it "bootstraps to public ip if it's set" do
server.public_ip = '8.8.8.8'
expect(stubbed_knife).to receive(:knife_bootstrap).with(output, '8.8.8.8', instance_of(Array))
bootstrap
end
context 'after successful bootstrap' do
it "updates server's chef node name in db" do
expect(stubbed_connector).to receive(:server_set_chef_node_name).with(instance_of(Devops::Model::Server))
bootstrap
end
end
context "if bootstraping wasn't successful" do
before { allow(stubbed_knife).to receive(:knife_bootstrap).and_return(123) }
it 'returns :server_bootstrap_fail code' do
expect(bootstrap).to eq 2
end
it "doesn't run after hook" do
expect(executor).to receive(:run_hook).with(:before_bootstrap, output)
bootstrap
end
end
end
end
describe '#two_phase_bootstrap', stubbed_knife: true do
let(:two_phase_bootstrap) { executor.two_phase_bootstrap({}) }
before do
allow(provider).to receive(:run_list) {[]}
allow(stubbed_connector).to receive(:server_delete)
end
context 'when bootstrap was successful' do
before do
allow(executor).to receive(:bootstrap) { 0 }
allow(executor).to receive(:check_server_on_chef_server) { false }
end
context 'if node presents on chef server' do
before do
allow(executor).to receive(:check_server_on_chef_server) { true }
allow(executor).to receive(:deploy_server)
allow(stubbed_knife).to receive(:set_run_list)
end
it 'builds run list' do
expect(executor).to receive(:compute_run_list)
two_phase_bootstrap
end
it 'sets run list to chef node' do
expect(stubbed_knife).to receive(:set_run_list)
two_phase_bootstrap
end
it 'deploys server' do
expect(executor).to receive(:deploy_server)
two_phase_bootstrap
end
context 'if deploy was successful' do
it 'returns 0' do
allow(executor).to receive(:deploy_server) { 0 }
expect(two_phase_bootstrap).to eq 0
end
end
context "if deploy wasn't successful" do
it 'returns :deploy_failed code' do
allow(executor).to receive(:deploy_server) { 1 }
expect(two_phase_bootstrap).to eq 8
end
end
context 'when an error occured during deploy' do
it 'returns :deploy_unknown_error code' do
allow(executor).to receive(:deploy_server) { raise }
expect(two_phase_bootstrap).to eq 6
end
end
end
context "if node doesn't present on chef server" do
it 'roll backs and then deletes server from mongo' do
allow(executor).to receive(:check_server_on_chef_server) { false }
allow(executor).to receive(:roll_back)
allow(stubbed_connector).to receive(:server_delete)
expect(executor).to receive(:roll_back).ordered
expect(stubbed_connector).to receive(:server_delete).ordered
two_phase_bootstrap
end
end
end
context "when bootstrap wasn't successful" do
it 'returns :server_bootstrap_fail error code' do
allow(executor).to receive(:bootstrap) { 1 }
expect(two_phase_bootstrap).to eq 2
end
end
context 'when an error occured during bootstrap' do
it 'returns :server_bootstrap_unknown_error error code' do
allow(executor).to receive(:bootstrap) { raise }
expect(two_phase_bootstrap).to eq 7
end
end
end
describe '#check_server_on_chef_server', stubbed_knife: true do
before do
server.chef_node_name = 'a'
allow(stubbed_knife).to receive(:chef_node_list) { @node_list }
allow(stubbed_knife).to receive(:chef_client_list) { @client_list }
end
it 'returns true when node_name in node list and in client list' do
@node_list = %w(a); @client_list = %w(a)
expect(executor.check_server_on_chef_server).to be true
end
it "returns false if node name isn't in node list" do
@node_list = []; @client_list = %w(a)
expect(executor.check_server_on_chef_server).to be false
end
it "returns false if node name isn't in node list" do
@node_list = %w(a); @client_list = []
expect(executor.check_server_on_chef_server).to be false
end
end
describe '#unbootstrap', stubbed_knife: true do
before do
allow(stubbed_connector).to receive_message_chain('key.path') { 'path_to_key' }
allow(stubbed_knife).to receive(:chef_node_delete)
allow(stubbed_knife).to receive(:chef_client_delete)
allow(executor).to receive(:execute_system_command) { '' }
allow(executor).to receive(:last_command_successful?) { true }
allow(executor).to receive(:sleep)
end
it 'deletes node from chef server' do
allow(executor).to receive(:delete_from_chef_server).and_call_original
expect(executor).to receive(:delete_from_chef_server)
executor.unbootstrap
end
it "uses server's key" do
expect(executor).to receive(:execute_system_command).with(%r(ssh -i path_to_key))
executor.unbootstrap
end
it 'backups /etc/chef' do
expect(executor).to receive(:execute_system_command).with(%r(mv /etc/chef ))
executor.unbootstrap
end
it 'returns a hash with :chef_node, :chef_client and :server keys' do
expect(executor.unbootstrap).to be_a(Hash).and include(:chef_node, :chef_client, :server)
end
it "writes successful message into result's :server key" do
expect(executor.unbootstrap[:server]).to include('renamed')
end
context "when command returned 'not found'" do
it "writes error message into result's :server key" do
allow(executor).to receive(:execute_system_command) { 'not found' }
expect(executor.unbootstrap[:server]).to eq("Directory '/etc/chef' does not exists")
end
end
context "if command wasn't successful" do
it 'returns hash with error after 5.retries' do
allow(executor).to receive(:last_command_successful?) { false }
expect(executor).to receive(:sleep).with(1).exactly(5).times
expect(executor.unbootstrap).to be_a(Hash).and include(:error)
end
end
end
describe '#deploy_server_with_tags', stubbed_knife: true do
let(:current_tags) { @current_tags }
let(:initial_tags) { %w(a b) }
let(:joined_initial_tags) { initial_tags.join(' ') }
let(:given_tags) { %w(c d) }
let(:joined_given_tags) { given_tags.join(' ') }
let(:deploy_server_with_tags) { executor.deploy_server_with_tags(given_tags, {}) }
before do
@current_tags = initial_tags.dup
allow(stubbed_knife).to receive(:tags_list) { @current_tags }
allow(stubbed_knife).to receive(:swap_tags) do |_, tags_to_delete, tags_to_add|
@current_tags -= tags_to_delete.split
@current_tags += tags_to_add.split
end
allow(executor).to receive(:deploy_server)
end
context 'when tags are empty' do
it 'just deploys server' do
expect(executor).to receive(:deploy_server)
expect(stubbed_knife).not_to receive(:swap_tags)
executor.deploy_server_with_tags([], {})
end
end
context 'when tags are not empty' do
it 'temporarily swaps current_tags with given ones, deploys server and then restores tags' do
expect(stubbed_knife).to receive(:tags_list).ordered
expect(stubbed_knife).to receive(:swap_tags).with(instance_of(String), joined_initial_tags, joined_given_tags).ordered
expect(executor).to receive(:deploy_server).ordered
expect(stubbed_knife).to receive(:swap_tags).with(instance_of(String), joined_given_tags, joined_initial_tags).ordered
deploy_server_with_tags
end
end
context 'if error occures during deploy' do
it 'restores tags anyway' do
allow(executor).to receive(:deploy_server) { raise }
expect {
deploy_server_with_tags
}.to raise_error StandardError
expect(current_tags).to eq initial_tags
end
end
context 'if cannot add tags to server' do
it 'returns :server_cannot_update_tags code' do
allow(stubbed_knife).to receive(:swap_tags) { false }
expect(deploy_server_with_tags).to eq 3
end
end
end
describe '#deploy_server', stubbed_knife: true do
let(:deploy_info) { @deploy_info }
let(:json_file_name) { 'json.json' }
let(:json_file_path) { File.join(SpecSupport.tmp_dir, json_file_name) }
let(:deploy_server) { executor.deploy_server(deploy_info) }
before do
allow(executor).to receive(:run_hook).with(:before_deploy, any_args)
allow(executor).to receive(:run_hook).with(:after_deploy, any_args)
allow(stubbed_knife).to receive(:ssh_stream) { 'Chef Client finished'}
allow(stubbed_connector).to receive(:key) { double('Key', path: 'path_to_key') }
allow(stubbed_connector).to receive(:server_update)
@deploy_info = {}
end
it 'runs before_deploy and after_deploy hooks' do
expect(executor).to receive(:run_hook).with(:before_deploy, any_args).ordered
expect(executor).to receive(:run_hook).with(:after_deploy, any_args).ordered
deploy_server
end
context 'when uses json file' do
before(:all) do
@tmp_files_at_start = Dir.entries(SpecSupport.tmp_dir)
end
before do
allow(DevopsConfig).to receive(:config).and_return({
project_info_dir: SpecSupport.tmp_dir,
address: 'host.com',
port: '8080',
url_prefix: 'api'
})
deploy_info['use_json_file'] = true
end
after(:all) do
diff = Dir.entries(SpecSupport.tmp_dir) - @tmp_files_at_start
diff.each do |file|
FileUtils.rm(File.join(SpecSupport.tmp_dir, file))
end
end
it 'writes deploy_info to json file if it not exists' do
expect { deploy_server }.to change { Dir.entries(SpecSupport.tmp_dir)}
end
it "writes deploy_info to given json file name if it doesn't exist" do
FileUtils.rm(json_file_path) if File.exists?(json_file_path)
deploy_info['json_file'] = json_file_name
expect { deploy_server }.to change {
Dir.entries(SpecSupport.tmp_dir)
}
FileUtils.rm(json_file_path)
end
it 'reads json from file if it exists' do
deploy_info['json_file'] = json_file_name
File.open(json_file_path, 'w') { |file| file.puts '{"foo": "bar"'}
expect { deploy_server }.not_to change {
Dir.entries(SpecSupport.tmp_dir)
}
FileUtils.rm(json_file_path)
end
it 'adds link to json to deploy command' do
deploy_info['json_file'] = json_file_name
regexp = %r(-j http://host.com:8080/api/v2.0/deploy/data/#{json_file_name})
expect(stubbed_knife).to receive(:ssh_stream).with(anything, regexp, any_args)
deploy_server
end
end
context "doesn't use json file" do
before do
deploy_info['use_json_file'] = false
deploy_info['run_list'] = %w(foo bar)
end
it "adds run list to command if server's stack is set" do
server.stack = 'stack'
expect(stubbed_knife).to receive(:ssh_stream).with(anything, %r(-r foo,bar), any_args)
deploy_server
end
it "doesn't add run list to command if server's stack is unset" do
expect(stubbed_knife).to receive(:ssh_stream).with(anything, 'chef-client --no-color', any_args)
deploy_server
end
end
it "uses server's key" do
expect(stubbed_connector).to receive(:key).with('key_id')
expect(stubbed_knife).to receive(:ssh_stream).with(any_args, 'path_to_key')
deploy_server
end
it "uses public ip if it's set" do
server.public_ip = '127.0.0.1'
expect(stubbed_knife).to receive(:ssh_stream).with(anything, anything, '127.0.0.1', any_args)
deploy_server
end
it "uses private_ip if public_ip isn't set" do
expect(stubbed_knife).to receive(:ssh_stream).with(anything, anything, server.private_ip, any_args)
deploy_server
end
context 'if deploy was successful' do
it "updates server's last operation" do
expect(server).to receive(:set_last_operation).with('deploy', anything)
expect(stubbed_connector).to receive(:server_update).with(server)
deploy_server
end
it 'returns 0' do
expect(deploy_server).to eq 0
end
end
context "when deploy wasn't successful" do
before { allow(stubbed_knife).to receive(:ssh_stream) { 'fail'} }
it "doesn't run after_deploy hook" do
expect(executor).to receive(:run_hook).with(:before_deploy, any_args)
expect(executor).not_to receive(:run_hook).with(:after_deploy, any_args)
deploy_server
end
it 'returns 1' do
expect(deploy_server).to eq 1
end
end
end
describe '#delete_from_chef_server', stubbed_knife: true do
let(:delete_from_chef_server) { executor.delete_from_chef_server('foo') }
before do
allow(stubbed_knife).to receive(:chef_client_delete)
allow(stubbed_knife).to receive(:chef_node_delete)
delete_from_chef_server
end
it 'returns hash with :chef_node and :chef_client keys' do
expect(delete_from_chef_server).to be_a(Hash).and include(:chef_node, :chef_client)
end
it 'calls to :chef_node_delete and :chef_client_delete' do
expect(stubbed_knife).to have_received(:chef_client_delete)
expect(stubbed_knife).to have_received(:chef_node_delete)
end
end
describe '#delete_server' do
let(:delete_server) { executor.delete_server }
context 'when server is static' do
before do
server.provider = 'static'
allow(stubbed_connector).to receive(:server_delete).with(server.id)
allow(executor).to receive(:unbootstrap)
end
it 'performs unbootstrap' do
expect(executor).to receive(:unbootstrap)
delete_server
end
it 'deletes server from mongo' do
expect(stubbed_connector).to receive(:server_delete).with(server.id)
delete_server
end
it 'returns message and nil' do
expect(delete_server.first).to be_a(String)
expect(delete_server.last).to be nil
end
it "doesn't try to remove it from cloud" do
expect{delete_server}.not_to raise_error
end
end
context "when server isn't static", stubbed_knife: true do
before do
allow(server).to receive_message_chain('provider_instance.delete_server')
allow(stubbed_connector).to receive(:server_delete).with(server.id)
allow(stubbed_knife).to receive(:chef_node_delete)
allow(stubbed_knife).to receive(:chef_client_delete)
end
it 'deletes from info about note chef server' do
allow(executor).to receive(:delete_from_chef_server).and_call_original
expect(executor).to receive(:delete_from_chef_server)
delete_server
end
it "doesn't unbootstrap server" do
expect(executor).not_to receive(:unbootstrap)
delete_server
end
it 'deletes server from cloud' do
expect(server).to receive_message_chain('provider_instance.delete_server').with(server)
delete_server
end
it "doesn't raise error if server wasn't found in cloud" do
allow(server).to receive_message_chain('provider_instance.name')
allow(server).to receive_message_chain('provider_instance.delete_server') {
raise Fog::Compute::OpenStack::NotFound
}
expect { delete_server }.not_to raise_error
end
it 'deletes server from mongo' do
expect(stubbed_connector).to receive(:server_delete).with(server.id)
delete_server
end
it 'returns message and hash with :chef_node, :chef_client and :server keys' do
expect(delete_server.first).to be_a(String)
expect(delete_server.last).to be_a(Hash).and include(:chef_client, :chef_node, :server)
end
end
end
describe '#rollback' do
before do
allow(executor).to receive(:delete_from_chef_server) { {} }
allow(server).to receive_message_chain('provider_instance.delete_server')
end
it "does nothing if server.id is nil" do
server.id = nil
expect(executor).not_to receive(:delete_from_chef_server)
expect(server).not_to receive(:provider_instance)
executor.roll_back
end
it 'deletes node from chef server and instance from cloud' do
expect(executor).to receive(:delete_from_chef_server)
expect(server).to receive_message_chain('provider_instance.delete_server')
executor.roll_back
end
it "doesn't raise if deleting server in cloud raises an error" do
allow(server).to receive_message_chain('provider_instance.delete_server') { raise }
expect { executor.roll_back }.not_to raise_error
end
end
describe '#add_run_list_to_deploy_info' do
it "doesn't change deploy info if it already includes run list" do
deploy_info = {'run_list' => %w(foo)}
expect {
executor.add_run_list_to_deploy_info(output, deploy_info)
}.not_to change { deploy_info }
end
it 'computes and adds run_list to deploy_info' do
deploy_info = {}
allow(executor).to receive(:compute_run_list) { %w(foo) }
expect(executor).to receive(:compute_run_list)
executor.add_run_list_to_deploy_info(output, deploy_info)
expect(deploy_info['run_list']).to eq %w(foo)
end
end
describe '#compute_run_list' do
before do
allow(deploy_env).to receive_message_chain('provider_instance.run_list') { %w(a) }
project.run_list = %w(b)
deploy_env.run_list = %w(c)
server.run_list = %w(d)
end
it "returns array with run list merged from provider's, project's, env's and server's run lists" do
expect(executor.compute_run_list).to be_an(Array).and contain_exactly(*%w(a b c d))
end
it "includes stack's run list if stack is set", stubbed_connector: true do
server.stack = 'stack'
allow(stubbed_connector).to receive(:stack) { instance_double(Devops::Model::StackEc2, run_list: %w(e)) }
expect(executor.compute_run_list).to be_an(Array).and contain_exactly(*%w(a b c d e))
end
it "doesn't contain nils" do
server.run_list = nil
server.stack = 'stack'
allow(stubbed_connector).to receive(:stack) { instance_double(Devops::Model::StackEc2, run_list: nil) }
expect(executor.compute_run_list).to be_an(Array).and contain_exactly(*%w(a b c))
end
it 'returns uniq elements' do
project.run_list = %w(a)
deploy_env.run_list = %w(a)
expect(executor.compute_run_list).to be_an(Array).and contain_exactly(*%w(a d))
end
end
end