diff --git a/.gitignore b/.gitignore index 4a2fd9cf..1ba9a25b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ RemoteSystemsTempFiles/ codedeploy-local.log* codedeploy-local.*.log deployment/ +.idea/ +.DS_STORE diff --git a/buildspec-agent-rake.yml b/buildspec-agent-rake.yml new file mode 100644 index 00000000..0b5d566f --- /dev/null +++ b/buildspec-agent-rake.yml @@ -0,0 +1,25 @@ +version: 0.2 + +phases: + install: + commands: + - echo Installing bundler + - gem install bundler + - echo Using bundler to install remaining gems + - bundle install + pre_build: + commands: + - echo Build started on `date` + - echo Running rake - unit tests + - rake + build: + commands: + - echo Build completed on `date` + - echo Running rake test-integration - integ tests + - rake test-integration + post_build: + commands: + - echo $CODEBUILD_SOURCE_VERSION > codebuild_source_commit_id +artifacts: + files: + - codebuild_source_commit_id diff --git a/codedeploy_agent-1.1.0.gemspec b/codedeploy_agent-1.1.0.gemspec index f7a03d90..e3138485 100644 --- a/codedeploy_agent-1.1.0.gemspec +++ b/codedeploy_agent-1.1.0.gemspec @@ -17,6 +17,7 @@ Gem::Specification.new do |spec| spec.add_dependency('aws-sdk-core', '~> 2.9') spec.add_dependency('simple_pid', '~> 0.2.1') spec.add_dependency('docopt', '~> 0.5.0') + spec.add_dependency('concurrent-ruby', '~> 1.0.5') spec.add_development_dependency('rake', '~> 10.0') spec.add_development_dependency('rspec', '~> 3.2.0') diff --git a/features/resources/sample_app_bundle_windows/appspec.yml b/features/resources/sample_app_bundle_windows/appspec.yml index 7cae4016..71e81cea 100644 --- a/features/resources/sample_app_bundle_windows/appspec.yml +++ b/features/resources/sample_app_bundle_windows/appspec.yml @@ -1,6 +1,10 @@ version: 0.0 os: windows hooks: + BeforeBlockTraffic: + - location: scripts/before_block_traffic.bat + AfterBlockTraffic: + - location: scripts/after_block_traffic.bat ApplicationStop: - location: scripts/application_stop.bat BeforeInstall: @@ -11,3 +15,7 @@ hooks: - location: scripts/application_start.bat ValidateService: - location: scripts/validate_service.bat + BeforeAllowTraffic: + - location: scripts/before_allow_traffic.bat + AfterAllowTraffic: + - location: scripts/after_allow_traffic.bat diff --git a/features/step_definitions/agent_steps.rb b/features/step_definitions/agent_steps.rb index 540ba11e..ad59b820 100644 --- a/features/step_definitions/agent_steps.rb +++ b/features/step_definitions/agent_steps.rb @@ -46,9 +46,6 @@ def eventually(options = {}, &block) @working_directory = Dir.mktmpdir configure_local_agent(@working_directory) - #Reset aws credentials to default location - Aws.config[:credentials] = Aws::SharedCredentials.new.credentials - #instantiate these clients first so they use user's aws creds instead of assumed role creds @codedeploy_client = Aws::CodeDeploy::Client.new @iam_client = Aws::IAM::Client.new @@ -60,8 +57,6 @@ def configure_local_agent(working_directory) InstanceAgent::Log.init(File.join(working_directory, 'codedeploy-agent.log')) InstanceAgent::Config.init InstanceAgent::Platform.util = StepConstants::IS_WINDOWS ? InstanceAgent::WindowsUtil : InstanceAgent::LinuxUtil - - if StepConstants::IS_WINDOWS then configure_windows_certificate end InstanceAgent::Config.config[:on_premises_config_file] = "#{working_directory}/codedeploy.onpremises.yml" configuration_contents = <<-CONFIG @@ -84,13 +79,6 @@ def configure_local_agent(working_directory) InstanceAgent::Config.load_config end -def configure_windows_certificate - cert_dir = File.expand_path(File.join(File.dirname(__FILE__), '..\certs')) - Aws.config[:ssl_ca_bundle] = File.join(cert_dir, 'windows-ca-bundle.crt') - ENV['AWS_SSL_CA_DIRECTORY'] = File.join(cert_dir, 'windows-ca-bundle.crt') - ENV['SSL_CERT_FILE'] = File.join(cert_dir, 'windows-ca-bundle.crt') -end - After("@codedeploy-agent") do @thread.kill unless @thread.nil? @codedeploy_client.delete_application({:application_name => @application_name}) unless @application_name.nil? @@ -234,12 +222,20 @@ def create_instance_role rescue Aws::IAM::Errors::EntityAlreadyExists #Using the existing role end - eventually do + + instance_role_arn = eventually do instance_role = @iam_client.get_role({:role_name => INSTANCE_ROLE_NAME}).role expect(instance_role).not_to be_nil expect(instance_role.assume_role_policy_document).not_to be_nil instance_role.arn end + + @iam_client.update_assume_role_policy({ + policy_document: instance_role_policy, + role_name: INSTANCE_ROLE_NAME, + }) + + instance_role_arn end def instance_role_policy diff --git a/features/step_definitions/codedeploy_local_steps.rb b/features/step_definitions/codedeploy_local_steps.rb index 592ac923..d12d2410 100644 --- a/features/step_definitions/codedeploy_local_steps.rb +++ b/features/step_definitions/codedeploy_local_steps.rb @@ -14,10 +14,6 @@ Before("@codedeploy-local") do @test_directory = Dir.mktmpdir - - #Reset aws credentials to default location - Aws.config[:credentials] = Aws::SharedCredentials.new.credentials - configure_local_agent(@test_directory) end @@ -60,7 +56,7 @@ def tar_app_bundle(temp_directory_to_create_bundle) #Unfortunately Minitar will keep pack all the file paths as given, so unless you change directories into the location where you want to pack the files the bundle won't have the correct files and folders Dir.chdir @bundle_original_directory_location - File.open(tar_file_name, 'wb') { |tar| Minitar.pack(directories_and_files_inside(@bundle_original_directory_location), tar) } + File.open(tar_file_name, 'wb') { |tar| Minitar.pack(InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.directories_and_files_inside(@bundle_original_directory_location), tar) } Dir.chdir old_direcory tar_file_name @@ -102,7 +98,7 @@ def tgz_app_bundle(temp_directory_to_create_bundle) File.open(tgz_file_name, 'wb') do |file| Zlib::GzipWriter.wrap(file) do |gz| - Minitar.pack(directories_and_files_inside(@bundle_original_directory_location), gz) + Minitar.pack(InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.directories_and_files_inside(@bundle_original_directory_location), gz) end end @@ -134,7 +130,10 @@ def create_local_deployment(custom_events = nil, file_exists_behavior = nil) codeedeploy_command_suffix = " --file-exists-behavior #{file_exists_behavior}" end - system "bin/codedeploy-local --bundle-location #{@bundle_location} --type #{@bundle_type} --deployment-group #{LOCAL_DEPLOYMENT_GROUP_ID} --agent-configuration-file #{InstanceAgent::Config.config[:config_file]}#{codeedeploy_command_suffix}" + # Windows doesn't respect shebang lines so ruby needs to be specified + ruby_prefix_for_windows = StepConstants::IS_WINDOWS ? "ruby " : "" + + system "#{ruby_prefix_for_windows}bin/codedeploy-local --bundle-location #{@bundle_location} --type #{@bundle_type} --deployment-group #{LOCAL_DEPLOYMENT_GROUP_ID} --agent-configuration-file #{InstanceAgent::Config.config[:config_file]}#{codeedeploy_command_suffix}" end Then(/^the local deployment command should succeed$/) do @@ -146,7 +145,7 @@ def create_local_deployment(custom_events = nil, file_exists_behavior = nil) end Then(/^the expected files should have have been locally deployed to my host(| twice)$/) do |maybe_twice| - deployment_ids = directories_and_files_inside("#{InstanceAgent::Config.config[:root_dir]}/#{LOCAL_DEPLOYMENT_GROUP_ID}") + deployment_ids = InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.directories_and_files_inside("#{InstanceAgent::Config.config[:root_dir]}/#{LOCAL_DEPLOYMENT_GROUP_ID}") step "the expected files in directory #{bundle_original_directory_location}/scripts should have have been deployed#{maybe_twice} to my host during deployment with deployment group id #{LOCAL_DEPLOYMENT_GROUP_ID} and deployment ids #{deployment_ids.join(' ')}" end diff --git a/features/step_definitions/common_steps.rb b/features/step_definitions/common_steps.rb index 50c11ac8..296d5a9a 100644 --- a/features/step_definitions/common_steps.rb +++ b/features/step_definitions/common_steps.rb @@ -3,24 +3,36 @@ $:.unshift File.join(File.dirname(File.expand_path('../..', __FILE__)), 'features') require 'step_definitions/step_constants' +@bucket_creation_count = 0; Given(/^I have a sample bundle uploaded to s3$/) do - s3 = Aws::S3::Client.new - - begin - s3.create_bucket({ - bucket: StepConstants::APP_BUNDLE_BUCKET, # required - create_bucket_configuration: { - location_constraint: Aws.config[:region], - } - }) - rescue Aws::S3::Errors::BucketAlreadyOwnedByYou - #Already created the bucket - end +=begin +This fails if the s3 upload is attempted after assume_role is called in the first integration test. +This is because once you call assume role the next time it instantiates a client it is using different permissions. In my opinion thats a bug because it doesn't match the documentation for the AWS SDK. +https://docs.aws.amazon.com/sdkforruby/api/index.html + +Their documentation says an assumed role is the LAST permission it will try to rely on but it looks like its always the first. But the s3 upload is the only place that this mattered so I simply forced this code so it doesn't do it again since the bundle is identical for both tests. +=end + if @bucket_creation_count == 0 + s3 = Aws::S3::Client.new + + begin + s3.create_bucket({ + bucket: StepConstants::APP_BUNDLE_BUCKET, # required + create_bucket_configuration: { + location_constraint: Aws.config[:region], + } + }) + rescue Aws::S3::Errors::BucketAlreadyOwnedByYou + #Already created the bucket + end - Dir.mktmpdir do |temp_directory_to_create_zip_file| - File.open(zip_app_bundle(temp_directory_to_create_zip_file), 'rb') do |file| - s3.put_object(bucket: StepConstants::APP_BUNDLE_BUCKET, key: StepConstants::APP_BUNDLE_KEY, body: file) + Dir.mktmpdir do |temp_directory_to_create_zip_file| + File.open(zip_app_bundle(temp_directory_to_create_zip_file), 'rb') do |file| + s3.put_object(bucket: StepConstants::APP_BUNDLE_BUCKET, key: StepConstants::APP_BUNDLE_KEY, body: file) + end end + + @bucket_creation_count += 1 end @bundle_type = 'zip' @@ -34,7 +46,7 @@ def zip_app_bundle(temp_directory_to_create_bundle) end def zip_directory(input_dir, output_file) - entries = directories_and_files_inside(input_dir) + entries = InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.directories_and_files_inside(input_dir) zip_io = Zip::File.open(output_file, Zip::File::CREATE) write_zip_entries(entries, '', input_dir, zip_io) @@ -47,7 +59,7 @@ def write_zip_entries(entries, path, input_dir, zip_io) diskFilePath = File.join(input_dir, zipFilePath) if File.directory?(diskFilePath) zip_io.mkdir(zipFilePath) - folder_entries = directories_and_files_inside(diskFilePath) + folder_entries = InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.directories_and_files_inside(diskFilePath) write_zip_entries(folder_entries, zipFilePath, input_dir, zip_io) else zip_io.get_output_stream(zipFilePath){ |f| f.write(File.open(diskFilePath, "rb").read())} @@ -55,36 +67,33 @@ def write_zip_entries(entries, path, input_dir, zip_io) end end -def directories_and_files_inside(directory) - Dir.entries(directory) - %w(.. .) -end Then(/^the expected files in directory (\S+) should have have been deployed(| twice) to my host during deployment with deployment group id (\S+) and deployment ids (.+)$/) do |expected_scripts_directory, maybe_twice, deployment_group_id, deployment_ids_space_separated| deployment_ids = deployment_ids_space_separated.split(' ') - directories_in_deployment_root_folder = directories_and_files_inside(InstanceAgent::Config.config[:root_dir]) - expect(directories_in_deployment_root_folder.size).to eq(3) + directories_in_deployment_root_folder = InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.directories_and_files_inside(InstanceAgent::Config.config[:root_dir]) + expect(directories_in_deployment_root_folder.size).to be >= 3 #ordering of the directories depends on the deployment group id, so using include instead of eq expect(directories_in_deployment_root_folder).to include(*%W(deployment-instructions deployment-logs #{deployment_group_id})) - files_in_deployment_logs_folder = directories_and_files_inside("#{InstanceAgent::Config.config[:root_dir]}/deployment-logs") + files_in_deployment_logs_folder = InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.directories_and_files_inside("#{InstanceAgent::Config.config[:root_dir]}/deployment-logs") expect(files_in_deployment_logs_folder.size).to eq(1) expect(files_in_deployment_logs_folder).to eq(%w(codedeploy-agent-deployments.log)) - directories_in_deployment_group_id_folder = directories_and_files_inside("#{InstanceAgent::Config.config[:root_dir]}/#{deployment_group_id}") + directories_in_deployment_group_id_folder = InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.directories_and_files_inside("#{InstanceAgent::Config.config[:root_dir]}/#{deployment_group_id}") expect(directories_in_deployment_group_id_folder.size).to eq(maybe_twice.empty? ? 1 : 2) expect(directories_in_deployment_group_id_folder).to eq(deployment_ids) deployment_id = deployment_ids.first - files_and_directories_in_deployment_id_folder = directories_and_files_inside("#{InstanceAgent::Config.config[:root_dir]}/#{deployment_group_id}/#{deployment_id}") + files_and_directories_in_deployment_id_folder = InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.directories_and_files_inside("#{InstanceAgent::Config.config[:root_dir]}/#{deployment_group_id}/#{deployment_id}") expect(files_and_directories_in_deployment_id_folder).to include(*%w(logs deployment-archive)) - files_and_directories_in_deployment_archive_folder = directories_and_files_inside("#{InstanceAgent::Config.config[:root_dir]}/#{deployment_group_id}/#{deployment_id}/deployment-archive") + files_and_directories_in_deployment_archive_folder = InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.directories_and_files_inside("#{InstanceAgent::Config.config[:root_dir]}/#{deployment_group_id}/#{deployment_id}/deployment-archive") expect(files_and_directories_in_deployment_archive_folder.size).to eq(2) expect(files_and_directories_in_deployment_archive_folder).to include(*%w(appspec.yml scripts)) - files_in_scripts_folder = directories_and_files_inside("#{InstanceAgent::Config.config[:root_dir]}/#{deployment_group_id}/#{deployment_id}/deployment-archive/scripts") - sample_app_bundle_script_files = directories_and_files_inside(expected_scripts_directory) + files_in_scripts_folder = InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.directories_and_files_inside("#{InstanceAgent::Config.config[:root_dir]}/#{deployment_group_id}/#{deployment_id}/deployment-archive/scripts") + sample_app_bundle_script_files = InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.directories_and_files_inside(expected_scripts_directory) expect(files_in_scripts_folder.size).to eq(sample_app_bundle_script_files.size) expect(files_in_scripts_folder).to include(*sample_app_bundle_script_files) end diff --git a/features/step_definitions/step_constants.rb b/features/step_definitions/step_constants.rb index f0002421..dfd19adc 100644 --- a/features/step_definitions/step_constants.rb +++ b/features/step_definitions/step_constants.rb @@ -5,9 +5,18 @@ class StepConstants ENV['AWS_REGION'] = Aws.config[:region] def self.current_aws_account + if StepConstants::IS_WINDOWS then StepConstants.configure_windows_certificate end + Aws::STS::Client.new.get_caller_identity.account end + def self.configure_windows_certificate + cert_dir = File.expand_path(File.join(File.dirname(__FILE__), '..\..\certs')) + Aws.config[:ssl_ca_bundle] = File.join(cert_dir, 'windows-ca-bundle.crt') + ENV['AWS_SSL_CA_DIRECTORY'] = File.join(cert_dir, 'windows-ca-bundle.crt') + ENV['SSL_CERT_FILE'] = File.join(cert_dir, 'windows-ca-bundle.crt') + end + CODEDEPLOY_TEST_PREFIX = "codedeploy-agent-integ-test-" unless defined?(CODEDEPLOY_TEST_PREFIX) IS_WINDOWS = (RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/) unless defined?(IS_WINDOWS) APP_BUNDLE_BUCKET_SUFFIX = IS_WINDOWS ? '-win' : '-linux' unless defined?(APP_BUNDLE_BUCKET_SUFFIX) diff --git a/lib/aws/codedeploy/local/cli_validator.rb b/lib/aws/codedeploy/local/cli_validator.rb index 19ebeeaf..12982067 100644 --- a/lib/aws/codedeploy/local/cli_validator.rb +++ b/lib/aws/codedeploy/local/cli_validator.rb @@ -52,7 +52,7 @@ def validate(args) end if events.include?('Install') && any_new_revision_event_before_install(events) - raise ValidationError.new("The only events that can be specified before Install are #{events_using_previous_successfuly_deployment_revision.push('DownloadBundle').join(',')}. Please fix the order of your specified events: #{args['--events']}") + raise ValidationError.new("The only events that can be specified before Install are #{events_using_previous_successfuly_deployment_revision.push('DownloadBundle', 'BeforeInstall').join(',')}. Please fix the order of your specified events: #{args['--events']}") end end @@ -79,7 +79,7 @@ def events_using_previous_successfuly_deployment_revision def events_using_new_revision InstanceAgent::Plugins::CodeDeployPlugin::HookExecutor::MAPPING_BETWEEN_HOOKS_AND_DEPLOYMENTS.select do |key,value| - value != InstanceAgent::Plugins::CodeDeployPlugin::HookExecutor::LAST_SUCCESSFUL_DEPLOYMENT + value != InstanceAgent::Plugins::CodeDeployPlugin::HookExecutor::LAST_SUCCESSFUL_DEPLOYMENT && key != 'BeforeInstall' end.keys end diff --git a/lib/instance_agent/config.rb b/lib/instance_agent/config.rb index 13319712..59a0bc0a 100644 --- a/lib/instance_agent/config.rb +++ b/lib/instance_agent/config.rb @@ -23,6 +23,7 @@ def initialize :pid_dir => nil, :shared_dir => nil, :user => nil, + :ongoing_deployment_tracking => 'ongoing-deployment', :children => 1, :http_read_timeout => 80, :instance_service_region => nil, @@ -31,6 +32,7 @@ def initialize :wait_between_runs => 30, :wait_after_error => 30, :codedeploy_test_profile => 'prod', + :kill_agent_max_wait_time_seconds => 7200, :on_premises_config_file => '/etc/codedeploy-agent/conf/codedeploy.onpremises.yml', :proxy_uri => nil, :enable_deployments_log => true diff --git a/lib/instance_agent/platform/linux_util.rb b/lib/instance_agent/platform/linux_util.rb index 094728a1..a4512774 100644 --- a/lib/instance_agent/platform/linux_util.rb +++ b/lib/instance_agent/platform/linux_util.rb @@ -67,6 +67,32 @@ def self.codedeploy_version_file def self.fallback_version_file "/opt/codedeploy-agent" end + + # shelling out the rm folder command to native os in this case linux. + def self.delete_dirs_command(dirs_to_delete) + log(:debug,"Dirs to delete: #{dirs_to_delete}"); + for dir in dirs_to_delete do + log(:debug,"Deleting dir: #{dir}"); + delete_folder(dir); + end + end + + private + def self.delete_folder (dir) + if dir != nil && dir != "/" + output = `rm -rf #{dir} 2>&1` + exit_status = $?.exitstatus + log(:debug, "Command status: #{$?}") + log(:debug, "Command output: #{output}") + unless exit_status == 0 + msg = "Error deleting directories: #{exit_status}" + log(:error, msg) + raise msg + end + else + log(:debug, "Empty directory or a wrong directory passed,#{dir}"); + end + end private def self.execute_tar_command(cmd) @@ -84,7 +110,7 @@ def self.execute_tar_command(cmd) raise msg end end - + private def self.log(severity, message) raise ArgumentError, "Unknown severity #{severity.inspect}" unless InstanceAgent::Log::SEVERITIES.include?(severity.to_s) diff --git a/lib/instance_agent/platform/windows_util.rb b/lib/instance_agent/platform/windows_util.rb index 719522c4..2980a155 100644 --- a/lib/instance_agent/platform/windows_util.rb +++ b/lib/instance_agent/platform/windows_util.rb @@ -32,10 +32,12 @@ def self.script_executable?(path) end def self.extract_tar(bundle_file, dst) + log(:warn, "Bundle format 'tar' not supported on Windows platforms. Bundle unpack may fail.") Minitar.unpack(bundle_file, dst) end def self.extract_tgz(bundle_file, dst) + log(:warn, "Bundle format 'tgz' not supported on Windows platforms. Bundle unpack may fail.") compressed = Zlib::GzipReader.open(bundle_file) Minitar.unpack(compressed, dst) end @@ -51,7 +53,33 @@ def self.codedeploy_version_file def self.fallback_version_file File.join(ENV['PROGRAMDATA'], "Amazon/CodeDeploy") end - + + # shelling out the rm folder command to native os in this case Window. + def self.delete_dirs_command(dirs_to_delete) + log(:debug,"Dirs to delete: #{dirs_to_delete}"); + for dir in dirs_to_delete do + log(:debug,"Deleting dir: #{dir}"); + delete_folder(dir); + end + end + + private + def self.delete_folder (dir) + if dir != nil && dir != "/" + output = `rd /s /q "#{dir}" 2>&1` + exit_status = $?.exitstatus + log(:debug, "Command status: #{$?}") + log(:debug, "Command output: #{output}") + unless exit_status == 0 + msg = "Error deleting directories: #{exit_status}" + log(:error, msg) + raise msg + end + else + log(:debug, "Empty directory or a wrong directory passed,#{dir}"); + end + end + private def self.log(severity, message) raise ArgumentError, "Unknown severity #{severity.inspect}" unless InstanceAgent::Log::SEVERITIES.include?(severity.to_s) diff --git a/lib/instance_agent/plugins/codedeploy/application_specification/application_specification.rb b/lib/instance_agent/plugins/codedeploy/application_specification/application_specification.rb index 9811d0bc..519b3ed9 100644 --- a/lib/instance_agent/plugins/codedeploy/application_specification/application_specification.rb +++ b/lib/instance_agent/plugins/codedeploy/application_specification/application_specification.rb @@ -1,5 +1,7 @@ require 'instance_agent/plugins/codedeploy/application_specification/script_info' require 'instance_agent/plugins/codedeploy/application_specification/file_info' +require 'instance_agent/plugins/codedeploy/application_specification/linux_permission_info' +require 'instance_agent/plugins/codedeploy/application_specification/mode_info' module InstanceAgent module Plugins diff --git a/lib/instance_agent/plugins/codedeploy/codedeploy_control.rb b/lib/instance_agent/plugins/codedeploy/codedeploy_control.rb index 26019d6d..c44d6e8b 100644 --- a/lib/instance_agent/plugins/codedeploy/codedeploy_control.rb +++ b/lib/instance_agent/plugins/codedeploy/codedeploy_control.rb @@ -80,27 +80,8 @@ def verify_cert client.proxy_pass = proxy_uri.password if proxy_uri.password end - client.verify_callback = lambda do |preverify_ok, cert_store| - return false unless preverify_ok - @cert = cert_store.chain[0] - verify_subject - end - response = client.get '/' end - - # Do minimal cert pinning - def verify_subject - InstanceAgent::Log.debug("#{self.class.to_s}: Actual certificate subject is '#{@cert.subject.to_s}'") - if "cn-north-1" == @region - @cert.subject.to_s == "/C=CN/ST=Beijing/L=Beijing/O=Amazon Connect Technology Services (Beijing) Co., Ltd./CN=codedeploy-commands."+@region+".amazonaws.com.cn" - elsif "cn-northwest-1" == @region - @cert.subject.to_s == "/C=CN/ST=Ningxia/L=Ningxia/O=Amazon Cloud Technology Services (Ningxia) Co., Ltd./CN=codedeploy-commands."+@region+".amazonaws.com.cn" - else - @cert.subject.to_s == "/C=US/ST=Washington/L=Seattle/O=Amazon.com, Inc./CN=codedeploy-commands."+@region+".amazonaws.com" - end - end - end end end diff --git a/lib/instance_agent/plugins/codedeploy/command_executor.rb b/lib/instance_agent/plugins/codedeploy/command_executor.rb index 75b998f5..3d1a1def 100644 --- a/lib/instance_agent/plugins/codedeploy/command_executor.rb +++ b/lib/instance_agent/plugins/codedeploy/command_executor.rb @@ -454,7 +454,8 @@ def cleanup_old_archives(deployment_spec) # Absolute path takes care of relative root directories directories = oldest_extra.map{ |f| File.absolute_path(f) } - FileUtils.rm_rf(directories) + log(:debug,"Delete Files #{directories}" ) + InstanceAgent::Platform.util.delete_dirs_command(directories) end diff --git a/lib/instance_agent/plugins/codedeploy/command_poller.rb b/lib/instance_agent/plugins/codedeploy/command_poller.rb index 11866ffd..e059576a 100644 --- a/lib/instance_agent/plugins/codedeploy/command_poller.rb +++ b/lib/instance_agent/plugins/codedeploy/command_poller.rb @@ -1,7 +1,9 @@ require 'socket' - +require 'concurrent' +require 'pathname' require 'instance_metadata' require 'instance_agent/agent/base' +require_relative 'deployment_command_tracker' module InstanceAgent module Plugins @@ -44,6 +46,13 @@ def initialize @plugin = InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor.new(:hook_mapping => DEFAULT_HOOK_MAPPING) + @thread_pool = Concurrent::ThreadPoolExecutor.new( + #TODO: Make these values configurable in agent configuration + min_threads: 1, + max_threads: 16, + max_queue: 0 # unbounded work queue + ) + log(:debug, "Initializing Host Agent: " + "Host Identifier = #{@host_identifier}") end @@ -59,18 +68,56 @@ def validate def perform return unless command = next_command + + #Commands will be executed on a separate thread. + begin + @thread_pool.post { + acknowledge_and_process_command(command) + } + rescue Concurrent::RejectedExecutionError + log(:warn, 'Graceful shutdown initiated, skipping any further polling until agent restarts') + end + end + + def graceful_shutdown + log(:info, "Gracefully shutting down agent child threads now, will wait up to #{ProcessManager::Config.config[:kill_agent_max_wait_time_seconds]} seconds") + # tell the pool to shutdown in an orderly fashion, allowing in progress work to complete + @thread_pool.shutdown + # now wait for all work to complete, wait till the timeout value + @thread_pool.wait_for_termination ProcessManager::Config.config[:kill_agent_max_wait_time_seconds] + log(:info, 'All agent child threads have been shut down') + end + + def acknowledge_and_process_command(command) return unless acknowledge_command(command) begin spec = get_deployment_specification(command) + process_command(command, spec) + #Commands that throw an exception will be considered to have failed + rescue Exception => e + log(:warn, 'Calling PutHostCommandComplete: "Code Error" ') + @deploy_control_client.put_host_command_complete( + :command_status => "Failed", + :diagnostics => {:format => "JSON", :payload => gather_diagnostics_from_error(e)}, + :host_command_identifier => command.host_command_identifier) + raise e + end + end + + def process_command(command, spec) + log(:debug, "Calling #{@plugin.to_s}.execute_command") + begin + deployment_id = InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification.parse(spec).deployment_id + DeploymentCommandTracker.create_ongoing_deployment_tracking_file(deployment_id) #Successful commands will complete without raising an exception - script_output = process_command(command, spec) + @plugin.execute_command(command, spec) + log(:debug, 'Calling PutHostCommandComplete: "Succeeded"') @deploy_control_client.put_host_command_complete( :command_status => 'Succeeded', :diagnostics => {:format => "JSON", :payload => gather_diagnostics()}, :host_command_identifier => command.host_command_identifier) - #Commands that throw an exception will be considered to have failed rescue ScriptError => e log(:debug, 'Calling PutHostCommandComplete: "Code Error" ') @@ -78,6 +125,7 @@ def perform :command_status => "Failed", :diagnostics => {:format => "JSON", :payload => gather_diagnostics_from_script_error(e)}, :host_command_identifier => command.host_command_identifier) + log(:error, "Error during perform: #{e.class} - #{e.message} - #{e.backtrace.join("\n")}") raise e rescue Exception => e log(:debug, 'Calling PutHostCommandComplete: "Code Error" ') @@ -85,10 +133,14 @@ def perform :command_status => "Failed", :diagnostics => {:format => "JSON", :payload => gather_diagnostics_from_error(e)}, :host_command_identifier => command.host_command_identifier) + log(:error, "Error during perform: #{e.class} - #{e.message} - #{e.backtrace.join("\n")}") raise e + ensure + DeploymentCommandTracker.delete_deployment_command_tracking_file(deployment_id) end end - + + private def next_command log(:debug, "Calling PollHostCommand:") output = @deploy_control_client.poll_host_command(:host_identifier => @host_identifier) @@ -107,6 +159,7 @@ def next_command command end + private def acknowledge_command(command) log(:debug, "Calling PutHostCommandAcknowledgement:") output = @deploy_control_client.put_host_command_acknowledgement( @@ -117,6 +170,7 @@ def acknowledge_command(command) true unless status == "Succeeded" || status == "Failed" end + private def get_deployment_specification(command) log(:debug, "Calling GetDeploymentSpecification:") output = @deploy_control_client.get_deployment_specification( @@ -129,11 +183,6 @@ def get_deployment_specification(command) output.deployment_specification.generic_envelope end - def process_command(command, spec) - log(:debug, "Calling #{@plugin.to_s}.execute_command") - @plugin.execute_command(command, spec) - end - private def gather_diagnostics_from_script_error(script_error) return script_error.to_json diff --git a/lib/instance_agent/plugins/codedeploy/deployment_command_tracker.rb b/lib/instance_agent/plugins/codedeploy/deployment_command_tracker.rb new file mode 100644 index 00000000..1bc2c473 --- /dev/null +++ b/lib/instance_agent/plugins/codedeploy/deployment_command_tracker.rb @@ -0,0 +1,65 @@ +require 'socket' +require 'concurrent' +require 'pathname' +require 'instance_metadata' +require 'instance_agent/agent/base' +require 'fileutils' +require 'instance_agent/log' + +module InstanceAgent + module Plugins + module CodeDeployPlugin + class FileDoesntExistException < Exception; end + class DeploymentCommandTracker + DEPLOYMENT_EVENT_FILE_STALE_TIMELIMIT_SECONDS = 86400 # 24 hour limit in secounds + + def self.create_ongoing_deployment_tracking_file(deployment_id) + FileUtils.mkdir_p(deployment_dir_path()) + FileUtils.touch(deployment_event_tracking_file_path(deployment_id)); + end + + def self.delete_deployment_tracking_file_if_stale?(deployment_id, timeout) + if(Time.now - File.ctime(deployment_event_tracking_file_path(deployment_id)) > timeout) + delete_deployment_command_tracking_file(deployment_id) + return true; + end + return false; + end + + def self.check_deployment_event_inprogress? + if(File.exists?deployment_dir_path()) + return directories_and_files_inside(deployment_dir_path()).any?{|deployment_id| check_if_lifecycle_event_is_stale?(deployment_id)} + else + return false + end + end + + def self.delete_deployment_command_tracking_file(deployment_id) + ongoing_deployment_event_file_path = deployment_event_tracking_file_path(deployment_id) + if File.exists?ongoing_deployment_event_file_path + File.delete(ongoing_deployment_event_file_path); + else + InstanceAgent::Log.warn("the tracking file does not exist") + end + end + + def self.directories_and_files_inside(directory) + Dir.entries(directory) - %w(.. .) + end + + private + def self.deployment_dir_path + File.join(InstanceAgent::Config.config[:root_dir], InstanceAgent::Config.config[:ongoing_deployment_tracking]) + end + + def self.check_if_lifecycle_event_is_stale?(deployment_id) + !delete_deployment_tracking_file_if_stale?(deployment_id,DEPLOYMENT_EVENT_FILE_STALE_TIMELIMIT_SECONDS) + end + + def self.deployment_event_tracking_file_path(deployment_id) + ongoing_deployment_file_path = File.join(deployment_dir_path(), deployment_id) + end + end + end + end +end \ No newline at end of file diff --git a/lib/instance_agent/plugins/codedeploy/deployment_specification.rb b/lib/instance_agent/plugins/codedeploy/deployment_specification.rb index 29c4c145..a7bbfb42 100644 --- a/lib/instance_agent/plugins/codedeploy/deployment_specification.rb +++ b/lib/instance_agent/plugins/codedeploy/deployment_specification.rb @@ -97,7 +97,9 @@ def initialize(data) raise 'Exactly one of S3Revision, GitHubRevision, or LocalRevision must be specified' end end - + # Decrypts the envelope /deployment specs + # Params: + # envelope: deployment specification thats to be cheked and decrypted def self.parse(envelope) raise 'Provided deployment spec was nil' if envelope.nil? @@ -115,11 +117,6 @@ def self.parse(envelope) # # The ruby wrapper returns true if OpenSSL returns 1 raise "Validation of PKCS7 signed message failed" unless pkcs7.verify([], @cert_store, nil, OpenSSL::PKCS7::NOCHAIN) - - signer_certs = pkcs7.certificates - raise "Validation of PKCS7 signed message failed" unless signer_certs.size == 1 - raise "Validation of PKCS7 signed message failed" unless verify_pkcs7_signer_cert(signer_certs[0]) - parse_deployment_spec_data(pkcs7.data) when "TEXT/JSON" raise "Unsupported DeploymentSpecification format: #{envelope.format}" unless AWS::CodeDeploy::Local::Deployer.running_as_developer_utility? @@ -172,32 +169,11 @@ def valid_local_bundle_type?(revision) revision.nil? || %w(tar zip tgz directory).any? { |k| revision["BundleType"] == k } end - def self.verify_pkcs7_signer_cert(cert) - @@region ||= ENV['AWS_REGION'] || InstanceMetadata.region - - # Do some minimal cert pinning - case InstanceAgent::Config.config()[:codedeploy_test_profile] - when 'beta', 'gamma' - cert.subject.to_s == "/C=US/ST=Washington/L=Seattle/O=Amazon.com, Inc./CN=codedeploy-signer-integ.amazonaws.com" - when 'prod' - if (@@region == "cn-north-1") - cert.subject.to_s == "/C=CN/ST=Beijing/L=Beijing/O=Amazon Connect Technology Services (Beijing) Co., Ltd./CN=codedeploy-signer-"+@@region+".amazonaws.com.cn" - elsif (@@region == "cn-northwest-1") - cert.subject.to_s == "/C=CN/ST=Ningxia/L=Ningxia/O=Amazon Cloud Technology Services (Ningxia) Co., Ltd./CN=codedeploy-signer-"+@@region+".amazonaws.com.cn" - else - cert.subject.to_s == "/C=US/ST=Washington/L=Seattle/O=Amazon.com, Inc./CN=codedeploy-signer-"+@@region+".amazonaws.com" - end - else - raise "Unknown profile '#{Config.config()[:codedeploy_test_profile]}'" - end - end - private def getDeploymentIdFromArn(arn) # example arn format: "arn:aws:codedeploy:us-east-1:123412341234:deployment/12341234-1234-1234-1234-123412341234" arn.split(":", 6)[5].split("/",2)[1] end - end end end diff --git a/lib/instance_agent/runner/child.rb b/lib/instance_agent/runner/child.rb index 7bf7f30e..ac96a293 100644 --- a/lib/instance_agent/runner/child.rb +++ b/lib/instance_agent/runner/child.rb @@ -1,5 +1,6 @@ # encoding: UTF-8 require 'process_manager/child' +require 'thread' module InstanceAgent module Runner @@ -38,6 +39,28 @@ def run runner.run end end + + # Stops the master after recieving the kill signal + # is overriden from ProcessManager::Daemon::Child + def stop + @runner.graceful_shutdown + ProcessManager::Log.info('agent exiting now') + super + end + + # Catches the trap signals and does a default or custom action + # is overriden from ProcessManager::Daemon::Child + def trap_signals + [:INT, :QUIT, :TERM].each do |sig| + trap(sig) do + ProcessManager::Log.info "#{description}: Received #{sig} - setting internal shutting down flag and possibly finishing last run" + stop_thread = Thread.new {stop} + stop_thread.join + end + end + # make sure we do not handle children like the master process + trap(:CHLD, 'DEFAULT') + end def description if runner diff --git a/lib/instance_agent/runner/master.rb b/lib/instance_agent/runner/master.rb index a72ddef7..902f15f9 100644 --- a/lib/instance_agent/runner/master.rb +++ b/lib/instance_agent/runner/master.rb @@ -1,13 +1,14 @@ # encoding: UTF-8 require 'process_manager/master' require 'instance_metadata' +require 'instance_agent/plugins/codedeploy/deployment_command_tracker' module InstanceAgent module Runner - class Master < ProcessManager::Daemon::Master + class DeploymentAlreadyInProgressException < Exception; end - ChildTerminationMaxWaitTime = 80 - + class Master < ProcessManager::Daemon::Master + def self.description(pid = $$) "master #{pid}" end @@ -28,17 +29,26 @@ def self.pid_file File.join(ProcessManager::Config.config[:pid_dir], "#{ProcessManager::Config.config[:program_name]}.pid") end + # Stops the master after recieving the kill signal + # is overriden from ProcessManager::Daemon::Master def stop if (pid = self.class.find_pid) - puts "Stopping #{description(pid)}" - ProcessManager::Log.info("Stopping #{description(pid)}") + puts "Checking first if a deployment is already in progress" + ProcessManager::Log.info("Checking first if any deployment lifecycle event is in progress #{description(pid)}") begin + if(InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.check_deployment_event_inprogress?) + ProcessManager::Log.info("Master process (#{pid}) will not be shut down right now, as a deployment is already in progress") + raise "A deployment is already in Progress",DeploymentAlreadyInProgressException + else + puts "Stopping #{description(pid)}" + ProcessManager::Log.info("Stopping #{description(pid)}") + end Process.kill('TERM', pid) rescue Errno::ESRCH end begin - Timeout.timeout(ChildTerminationMaxWaitTime) do + Timeout.timeout(ProcessManager::Config.config[:kill_agent_max_wait_time_seconds]) do loop do begin Process.kill(0, pid) @@ -66,7 +76,7 @@ def kill_children(sig) end begin - Timeout.timeout(ChildTerminationMaxWaitTime) do + Timeout.timeout(ProcessManager::Config.config[:kill_agent_max_wait_time_seconds]) do children.each do |index, child_pid| begin Process.wait(child_pid) diff --git a/lib/winagent.rb b/lib/winagent.rb index 2141e80c..e737bf87 100644 --- a/lib/winagent.rb +++ b/lib/winagent.rb @@ -55,9 +55,10 @@ def service_main end def service_stop - log(:info, 'stopping') + log(:info, 'stopping the agent') @polling_mutex.synchronize do - log(:info, 'exited') + @runner.graceful_shutdown + log(:info, 'agent exiting now') end end diff --git a/spec/aws/codedeploy/local/cli_validator_spec.rb b/spec/aws/codedeploy/local/cli_validator_spec.rb index 4b6cad4a..2216abac 100644 --- a/spec/aws/codedeploy/local/cli_validator_spec.rb +++ b/spec/aws/codedeploy/local/cli_validator_spec.rb @@ -208,8 +208,24 @@ allow(File).to receive(:exists?).with(FAKE_DIRECTORY).and_return(true) allow(File).to receive(:directory?).with(FAKE_DIRECTORY).and_return(true) expect(File).to receive(:exists?).with("#{FAKE_DIRECTORY}/appspec.yml").and_return(true) - expect{validator.validate(args)}.to raise_error(AWS::CodeDeploy::Local::CLIValidator::ValidationError, "The only events that can be specified before Install are BeforeBlockTraffic,AfterBlockTraffic,ApplicationStop,DownloadBundle. Please fix the order of your specified events: #{args['--events']}") + expect{validator.validate(args)}.to raise_error(AWS::CodeDeploy::Local::CLIValidator::ValidationError, "The only events that can be specified before Install are BeforeBlockTraffic,AfterBlockTraffic,ApplicationStop,DownloadBundle,BeforeInstall. Please fix the order of your specified events: #{args['--events']}") end end + + context 'when BeforeInstall event specified before Install' do + let(:args) do + {"--bundle-location"=>FAKE_DIRECTORY, + "--type"=>'directory', + '--events'=>'BeforeInstall,Install' + } + end + + it 'returns the same arguments' do + allow(File).to receive(:exists?).with(FAKE_DIRECTORY).and_return(true) + allow(File).to receive(:directory?).with(FAKE_DIRECTORY).and_return(true) + expect(File).to receive(:exists?).with("#{FAKE_DIRECTORY}/appspec.yml").and_return(true) + expect(validator.validate(args)).to equal(args) + end + end end end diff --git a/spec/aws/codedeploy/plugins/deployment_command_tracker_spec.rb b/spec/aws/codedeploy/plugins/deployment_command_tracker_spec.rb new file mode 100644 index 00000000..d602194d --- /dev/null +++ b/spec/aws/codedeploy/plugins/deployment_command_tracker_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' +require 'instance_agent' +require 'instance_agent/config' +require 'instance_agent/plugins/codedeploy/deployment_command_tracker' +require 'instance_agent/log' + +describe InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker do + describe '.create_ongoing_deployment_tracking_file' do + $deployment_id = 'D-123' + deployment_command_tracker = InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker; + context "when the deployment life cycle event is in progress" do + before do + InstanceAgent::Config.config[:root_dir] = File.join(Dir.tmpdir(), 'codeDeploytest') + InstanceAgent::Config.config[:ongoing_deployment_tracking] = 'ongoing-deployment' + InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.create_ongoing_deployment_tracking_file($deployment_id) + end + it 'tries to create ongoing-deployment folder' do + directories_in_deployment_root_folder = deployment_command_tracker.directories_and_files_inside(InstanceAgent::Config.config[:root_dir]); + expect(directories_in_deployment_root_folder).to include(InstanceAgent::Config.config[:ongoing_deployment_tracking]); + end + it 'creates ongoing-deployment file in the tracking folder' do + files_in_deployment_tracking_folder = deployment_command_tracker.directories_and_files_inside(File.join(InstanceAgent::Config.config[:root_dir], InstanceAgent::Config.config[:ongoing_deployment_tracking])) + expect(files_in_deployment_tracking_folder).to include($deployment_id); + end + end + end + describe '.check_deployment_event_inprogress' do + context 'when no deployment life cycle event is in progress' do + before do + InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.delete_deployment_command_tracking_file($deployment_id) + end + it 'checks if any deployment event is in progress' do + expect(InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.check_deployment_event_inprogress?).to equal(false); + end + end + context 'when deployment life cycle event is in progress' do + before do + InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.create_ongoing_deployment_tracking_file($deployment_id) + end + it 'checks if any deployment life cycle event is in progress ' do + expect(InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.check_deployment_event_inprogress?).to equal(true) + end + end + context 'when the agent starts for the first time' do + before do + FileUtils.rm_r(File.join(InstanceAgent::Config.config[:root_dir], InstanceAgent::Config.config[:ongoing_deployment_tracking])) + end + it 'checks if any deployment life cycle event is in progress ' do + expect(InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.check_deployment_event_inprogress?).to equal(false) + end + end + end + describe '.delete_deployment_tracking_file_if_stale' do + context 'when deployment life cycle event is in progress' do + before do + InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.create_ongoing_deployment_tracking_file($deployment_id) + end + it 'checks if the file is stale or not' do + expect(InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.delete_deployment_tracking_file_if_stale?($deployment_id, 2000)).to equal(false) + end + end + context 'when the wait-time has been more than the timeout time' do + it 'checks if the file is stale after the timeout' do + sleep 10 + expect(InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.delete_deployment_tracking_file_if_stale?($deployment_id, 5)).to equal(true) + end + end + end +end \ No newline at end of file diff --git a/test/instance_agent/config_test.rb b/test/instance_agent/config_test.rb index 2674cd8e..2b53bbaa 100644 --- a/test/instance_agent/config_test.rb +++ b/test/instance_agent/config_test.rb @@ -28,8 +28,10 @@ class InstanceAgentConfigTest < InstanceAgentTestCase :wait_after_error => 30, :codedeploy_test_profile => 'prod', :on_premises_config_file => '/etc/codedeploy-agent/conf/codedeploy.onpremises.yml', + :ongoing_deployment_tracking => 'ongoing-deployment', :proxy_uri => nil, - :enable_deployments_log => true + :enable_deployments_log => true, + :kill_agent_max_wait_time_seconds => 7200 }, InstanceAgent::Config.config) end diff --git a/test/instance_agent/plugins/codedeploy/command_poller_test.rb b/test/instance_agent/plugins/codedeploy/command_poller_test.rb index 0756ab65..38da1e91 100644 --- a/test/instance_agent/plugins/codedeploy/command_poller_test.rb +++ b/test/instance_agent/plugins/codedeploy/command_poller_test.rb @@ -1,8 +1,10 @@ require 'test_helper' require 'json' +require 'instance_agent/log' -class CommandPollerTest < InstanceAgentTestCase +class CommandPollerTest < InstanceAgentTestCase + include InstanceAgent::Plugins::CodeDeployPlugin def gather_diagnostics_from_error(error) {'error_code' => InstanceAgent::Plugins::CodeDeployPlugin::ScriptError::UNKNOWN_ERROR_CODE, 'script_name' => "", 'message' => error.message, 'log' => ""}.to_json end @@ -110,6 +112,12 @@ def gather_diagnostics(script_output) starts_as('setup') @deploy_control_client.stubs(:put_host_command_complete). when(@put_host_command_complete_state.is('setup')) + @deployment_id = stub(:deployment_id => "D-1234") + InstanceAgent::Config.config[:root_dir] = File.join(Dir.tmpdir(), "CodeDeploy") + InstanceAgent::Config.config[:ongoing_deployment_tracking] = "ongoing-deployment" + InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification.stubs(:parse).returns(@deployment_id) + InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.stubs(:delete_deployment_command_tracking_file).returns(true) + InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.stubs(:create_ongoing_deployment_tracking_file).returns(true) end should 'call PollHostCommand with the current host name' do @@ -253,7 +261,7 @@ def gather_diagnostics(script_output) :host_command_identifier => @command.host_command_identifier). returns(@poll_host_command_acknowledgement_output) - @poller.perform + @poller.acknowledge_and_process_command(@command) end should 'return when Succeeded command status is given by PollHostCommandAcknowledgement' do @@ -269,7 +277,7 @@ def gather_diagnostics(script_output) @deploy_control_client.expects(:put_host_command_complete).never. when(@put_host_command_complete_state.is('never')) - @poller.perform + @poller.acknowledge_and_process_command(@command) end should 'return when Failed command status is given by PollHostCommandAcknowledgement' do @@ -285,7 +293,7 @@ def gather_diagnostics(script_output) @deploy_control_client.expects(:put_host_command_complete).never. when(@put_host_command_complete_state.is('never')) - @poller.perform + @poller.acknowledge_and_process_command(@command) end should 'call GetDeploymentSpecification with the host ID and execution ID of the command' do @@ -294,7 +302,7 @@ def gather_diagnostics(script_output) :host_identifier => @host_identifier). returns(@get_deploy_specification_output) - @poller.perform + @poller.acknowledge_and_process_command(@command) end should 'allow exceptions from GetDeploymentSpecification to propagate to caller' do @@ -302,7 +310,7 @@ def gather_diagnostics(script_output) raises("some error") assert_raise "some error" do - @poller.perform + @poller.acknowledge_and_process_command(@command) end end @@ -315,11 +323,9 @@ def gather_diagnostics(script_output) should 'not dispatch the command to the command executor' do @execute_command_state.become('never') - @executor.expects(:execute_command).never. - when(@execute_command_state.is('never')) assert_raise do - @poller.perform + @poller.acknowledge_and_process_command(@command) end end @@ -331,7 +337,7 @@ def gather_diagnostics(script_output) :host_command_identifier => @command.host_command_identifier) assert_raise do - @poller.perform + @poller.acknowledge_and_process_command(@command) end end @@ -346,11 +352,9 @@ def gather_diagnostics(script_output) should 'not dispatch the command to the command executor' do @execute_command_state.become('never') - @executor.expects(:execute_command).never. - when(@execute_command_state.is('never')) assert_raise do - @poller.perform + @poller.acknowledge_and_process_command(@command) end end @@ -361,7 +365,7 @@ def gather_diagnostics(script_output) :host_command_identifier => @command.host_command_identifier) assert_raise do - @poller.perform + @poller.acknowledge_and_process_command(@command) end end @@ -376,11 +380,9 @@ def gather_diagnostics(script_output) should 'not dispatch the command to the command executor' do @execute_command_state.become('never') - @executor.expects(:execute_command).never. - when(@execute_command_state.is('never')) assert_raise do - @poller.perform + @poller.acknowledge_and_process_command(@command) end end @@ -391,7 +393,7 @@ def gather_diagnostics(script_output) :host_command_identifier => @command.host_command_identifier) assert_raise do - @poller.perform + @poller.acknowledge_and_process_command(@command) end end @@ -401,20 +403,19 @@ def gather_diagnostics(script_output) @executor.expects(:execute_command). with(@command, @deployment_specification.generic_envelope) - @poller.perform + @poller.process_command(@command, @deployment_specification.generic_envelope) end should 'allow exceptions from execute_command to propagate to caller' do @executor.expects(:execute_command). - raises("some error") - + raises("some error") @deploy_control_client.expects(:put_host_command_complete). with(:command_status => "Failed", :diagnostics => {:format => "JSON", :payload => gather_diagnostics_from_error(RuntimeError.new("some error"))}, :host_command_identifier => @command.host_command_identifier) assert_raise "some error" do - @poller.perform + @poller.process_command(@command, @deployment_specification.generic_envelope) end end @@ -436,7 +437,7 @@ def gather_diagnostics(script_output) :host_command_identifier => @command.host_command_identifier) assert_raise script_error do - @poller.perform + @poller.process_command(@command, @deployment_specification.generic_envelope) end end @@ -449,9 +450,23 @@ def gather_diagnostics(script_output) :diagnostics => {:format => "JSON", :payload => gather_diagnostics("")}, :host_command_identifier => @command.host_command_identifier) + @poller.process_command(@command, @deployment_specification.generic_envelope) + end + + should 'call Thread.new to spin off a new thread for executing commands' do + Thread.expects(:new).returns(nil) + @poller.perform end + should 'not try to enter thread if rejected execution error is raised (happens after shutdown initiated)' do + Thread.expects(:new).never + @mock_thread_pool_executor = mock() + Concurrent::ThreadPoolExecutor.expects(:new).returns(@mock_thread_pool_executor) + @mock_thread_pool_executor.stubs(:post).raises(Concurrent::RejectedExecutionError.new('RejectedExecutionError')) + + poller = InstanceAgent::Plugins::CodeDeployPlugin::CommandPoller.new + end end end diff --git a/test/instance_agent/plugins/codedeploy/install_instruction_test.rb b/test/instance_agent/plugins/codedeploy/install_instruction_test.rb index 6be719eb..9fd8821d 100644 --- a/test/instance_agent/plugins/codedeploy/install_instruction_test.rb +++ b/test/instance_agent/plugins/codedeploy/install_instruction_test.rb @@ -363,7 +363,7 @@ class InstallInstructionTest < InstanceAgentTestCase setup do @command_builder = CommandBuilder.new() @command_builder.copy("source", "destination") - @expected_json = {"instructions"=>[{"type"=>"copy","source"=>"source","destination"=>"#{Dir.tmpdir()}/destination"}]}.to_json + @expected_json = {"instructions"=>[{"type"=>"copy","source"=>"source","destination"=>"#{File.realdirpath(Dir.tmpdir())}/destination"}]}.to_json end should "have a single copy in the returned JSON" do @@ -371,7 +371,7 @@ class InstallInstructionTest < InstanceAgentTestCase end should "raise a duplicate exception when a copy collides with another copy" do - assert_raised_with_message("The deployment failed because the application specification file specifies two source files named source and source for the same destination (#{Dir.tmpdir()}/destination). Remove one of the source file paths from the AppSpec file, and then try again.") do + assert_raised_with_message("The deployment failed because the application specification file specifies two source files named source and source for the same destination (#{File.realdirpath(Dir.tmpdir())}/destination). Remove one of the source file paths from the AppSpec file, and then try again.") do @command_builder.copy("source", "destination") end end @@ -381,7 +381,7 @@ class InstallInstructionTest < InstanceAgentTestCase setup do @command_builder = CommandBuilder.new() @command_builder.mkdir("directory") - @expected_json = {"instructions"=>[{"type"=>"mkdir","directory"=>"#{Dir.tmpdir()}/directory"}]}.to_json + @expected_json = {"instructions"=>[{"type"=>"mkdir","directory"=>"#{File.realdirpath(Dir.tmpdir())}/directory"}]}.to_json end should "have a single mkdir in the returned JSON" do @@ -390,7 +390,7 @@ class InstallInstructionTest < InstanceAgentTestCase should "raise a duplicate exception when trying to create a directory collides with a copy" do @command_builder.copy("source", "directory/dir1") - assert_raised_with_message("The deployment failed because the application specification file includes an mkdir command more than once for the same destination path (#{Dir.tmpdir()}/directory/dir1) from (source). Update the files section of the AppSpec file, and then try again.") do + assert_raised_with_message("The deployment failed because the application specification file includes an mkdir command more than once for the same destination path (#{File.realdirpath(Dir.tmpdir())}/directory/dir1) from (source). Update the files section of the AppSpec file, and then try again.") do @command_builder.mkdir("directory/dir1") end end @@ -404,71 +404,71 @@ class InstallInstructionTest < InstanceAgentTestCase end should "raise a duplicate exception when trying to make a copy collides with a mkdir" do - assert_raised_with_message("The deployment failed because the application specification file calls for installing the file target, but a file with that name already exists at the location (#{Dir.tmpdir()}/directory/target). Update your AppSpec file or directory structure, and then try again.") do + assert_raised_with_message("The deployment failed because the application specification file calls for installing the file target, but a file with that name already exists at the location (#{File.realdirpath(Dir.tmpdir())}/directory/target). Update your AppSpec file or directory structure, and then try again.") do @command_builder.copy( "target", "directory/target") end end should "say it is copying the appropriate file" do - assert @command_builder.copying_file?("#{Dir.tmpdir()}/directory/target/file_target") - assert !@command_builder.copying_file?("#{Dir.tmpdir()}/directory/target") + assert @command_builder.copying_file?("#{File.realdirpath(Dir.tmpdir())}/directory/target/file_target") + assert !@command_builder.copying_file?("#{File.realdirpath(Dir.tmpdir())}/directory/target") end should "say it is making the appropriate directory" do - assert !@command_builder.making_directory?("#{Dir.tmpdir()}/directory/target/file_target") - assert @command_builder.making_directory?("#{Dir.tmpdir()}/directory/target") + assert !@command_builder.making_directory?("#{File.realdirpath(Dir.tmpdir())}/directory/target/file_target") + assert @command_builder.making_directory?("#{File.realdirpath(Dir.tmpdir())}/directory/target") end should "match the file when appropriate" do - permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("#{Dir.tmpdir()}/directory/target", { + permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("#{File.realdirpath(Dir.tmpdir())}/directory/target", { :type => ["file"], :pattern => "file*", :except => []}) - assert @command_builder.find_matches(permission).include?("#{Dir.tmpdir()}/directory/target/file_target") + assert @command_builder.find_matches(permission).include?("#{File.realdirpath(Dir.tmpdir())}/directory/target/file_target") - permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("#{Dir.tmpdir()}/directory/target", { + permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("#{File.realdirpath(Dir.tmpdir())}/directory/target", { :type => ["directory"], :pattern => "file*", :except => []}) - assert !@command_builder.find_matches(permission).include?("#{Dir.tmpdir()}/directory/target/file_target") + assert !@command_builder.find_matches(permission).include?("#{File.realdirpath(Dir.tmpdir())}/directory/target/file_target") - permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("#{Dir.tmpdir()}/directory/target", { + permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("#{File.realdirpath(Dir.tmpdir())}/directory/target", { :type => ["file"], :pattern => "filefile*", :except => []}) - assert !@command_builder.find_matches(permission).include?("#{Dir.tmpdir()}/directory/target/file_target") + assert !@command_builder.find_matches(permission).include?("#{File.realdirpath(Dir.tmpdir())}/directory/target/file_target") - permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("#{Dir.tmpdir()}/directory/target", { + permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("#{File.realdirpath(Dir.tmpdir())}/directory/target", { :type => ["file"], :pattern => "file*", :except => ["*target"]}) - assert !@command_builder.find_matches(permission).include?("#{Dir.tmpdir()}/directory/target/file_target") + assert !@command_builder.find_matches(permission).include?("#{File.realdirpath(Dir.tmpdir())}/directory/target/file_target") end should "match the directory when appropriate" do - permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("#{Dir.tmpdir()}/directory/", { + permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("#{File.realdirpath(Dir.tmpdir())}/directory/", { :type => ["directory"], :pattern => "tar*", :except => []}) - assert @command_builder.find_matches(permission).include?("#{Dir.tmpdir()}/directory/target") + assert @command_builder.find_matches(permission).include?("#{File.realdirpath(Dir.tmpdir())}/directory/target") - permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("#{Dir.tmpdir()}/directory/", { + permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("#{File.realdirpath(Dir.tmpdir())}/directory/", { :type => ["file"], :pattern => "tar*", :except => []}) - assert !@command_builder.find_matches(permission).include?("#{Dir.tmpdir()}/directory/target") + assert !@command_builder.find_matches(permission).include?("#{File.realdirpath(Dir.tmpdir())}/directory/target") - permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("#{Dir.tmpdir()}/directory/", { + permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("#{File.realdirpath(Dir.tmpdir())}/directory/", { :type => ["directory"], :pattern => "tarr*", :except => []}) - assert !@command_builder.find_matches(permission).include?("#{Dir.tmpdir()}/directory/target") + assert !@command_builder.find_matches(permission).include?("#{File.realdirpath(Dir.tmpdir())}/directory/target") - permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("#{Dir.tmpdir()}/directory/", { + permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("#{File.realdirpath(Dir.tmpdir())}/directory/", { :type => ["directory"], :pattern => "tar*", :except => ["*et"]}) - assert !@command_builder.find_matches(permission).include?("#{Dir.tmpdir()}/directory/target") + assert !@command_builder.find_matches(permission).include?("#{File.realdirpath(Dir.tmpdir())}/directory/target") end end @@ -477,7 +477,7 @@ class InstallInstructionTest < InstanceAgentTestCase @command_builder = CommandBuilder.new() @command_builder.mkdir("directory") @command_builder.mkdir("directory") - @expected_json = {"instructions"=>[{"type"=>"mkdir","directory"=>"#{Dir.tmpdir()}/directory"}]}.to_json + @expected_json = {"instructions"=>[{"type"=>"mkdir","directory"=>"#{File.realdirpath(Dir.tmpdir())}/directory"}]}.to_json end should "have a single mkdir in the returned JSON" do @@ -490,7 +490,7 @@ class InstallInstructionTest < InstanceAgentTestCase @command_builder = CommandBuilder.new() @command_builder.mkdir("directory") @command_builder.mkdir("directory/") - @expected_json = {"instructions"=>[{"type"=>"mkdir","directory"=>"#{Dir.tmpdir()}/directory"}]}.to_json + @expected_json = {"instructions"=>[{"type"=>"mkdir","directory"=>"#{File.realdirpath(Dir.tmpdir())}/directory"}]}.to_json end should "have a single mkdir in the returned JSON" do @@ -503,7 +503,7 @@ class InstallInstructionTest < InstanceAgentTestCase @permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("testfile.txt") @command_builder = CommandBuilder.new() @command_builder.set_permissions("testfile.txt", @permission) - assert_raised_with_message("The deployment failed because the permissions setting for (#{Dir.tmpdir()}/testfile.txt) is specified more than once in the application specification file. Update the files section of the AppSpec file, and then try again.") do + assert_raised_with_message("The deployment failed because the permissions setting for (#{File.realdirpath(Dir.tmpdir())}/testfile.txt) is specified more than once in the application specification file. Update the files section of the AppSpec file, and then try again.") do @command_builder.set_permissions("testfile.txt", @permission) end end @@ -525,10 +525,10 @@ class InstallInstructionTest < InstanceAgentTestCase :group=>"dev"}) @command_builder = CommandBuilder.new() @command_builder.set_permissions("testfile.txt", @permission) - @expected_json = {"instructions"=>[{"type"=>"chmod","mode"=>"744","file"=>"#{Dir.tmpdir()}/testfile.txt"}, - {"type"=>"setfacl","acl"=>["user:bob:rwx","default:group:dev:r--"],"file"=>"#{Dir.tmpdir()}/testfile.txt"}, - {"type"=>"semanage","context"=>{"user"=>"name","role"=>nil,"type"=>"type","range"=>"s2-s3:c0,c2.c4,c6"},"file"=>"#{Dir.tmpdir()}/testfile.txt"}, - {"type"=>"chown","owner"=>"bob","group"=>"dev","file"=>"#{Dir.tmpdir()}/testfile.txt"} + @expected_json = {"instructions"=>[{"type"=>"chmod","mode"=>"744","file"=>"#{File.realdirpath(Dir.tmpdir())}/testfile.txt"}, + {"type"=>"setfacl","acl"=>["user:bob:rwx","default:group:dev:r--"],"file"=>"#{File.realdirpath(Dir.tmpdir())}/testfile.txt"}, + {"type"=>"semanage","context"=>{"user"=>"name","role"=>nil,"type"=>"type","range"=>"s2-s3:c0,c2.c4,c6"},"file"=>"#{File.realdirpath(Dir.tmpdir())}/testfile.txt"}, + {"type"=>"chown","owner"=>"bob","group"=>"dev","file"=>"#{File.realdirpath(Dir.tmpdir())}/testfile.txt"} ]}.to_json assert_equal JSON.parse(@expected_json), JSON.parse(@command_builder.to_json) end @@ -537,7 +537,7 @@ class InstallInstructionTest < InstanceAgentTestCase @permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("testfile.txt", {:owner=>"bob"}) @command_builder = CommandBuilder.new() @command_builder.set_permissions("testfile.txt", @permission) - @expected_json = {"instructions"=>[{"type"=>"chown","owner"=>"bob","group"=>nil,"file"=>"#{Dir.tmpdir()}/testfile.txt"}]}.to_json + @expected_json = {"instructions"=>[{"type"=>"chown","owner"=>"bob","group"=>nil,"file"=>"#{File.realdirpath(Dir.tmpdir())}/testfile.txt"}]}.to_json assert_equal JSON.parse(@expected_json), JSON.parse(@command_builder.to_json) end @@ -545,7 +545,7 @@ class InstallInstructionTest < InstanceAgentTestCase @permission = InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::LinuxPermissionInfo.new("testfile.txt", {:group=>"dev"}) @command_builder = CommandBuilder.new() @command_builder.set_permissions("testfile.txt", @permission) - @expected_json = {"instructions"=>[{"type"=>"chown","owner"=>nil,"group"=>"dev","file"=>"#{Dir.tmpdir()}/testfile.txt"}]}.to_json + @expected_json = {"instructions"=>[{"type"=>"chown","owner"=>nil,"group"=>"dev","file"=>"#{File.realdirpath(Dir.tmpdir())}/testfile.txt"}]}.to_json assert_equal JSON.parse(@expected_json), JSON.parse(@command_builder.to_json) end end