From 0ec49fc6ef348ecdf595c907fb5013c5766e3ec8 Mon Sep 17 00:00:00 2001 From: nyo Date: Thu, 3 Sep 2020 21:19:07 +0200 Subject: [PATCH 1/7] Added cli-validator tests Fixed typo --- lib/aws/codedeploy/local/cli_validator.rb | 10 ++++++--- .../codedeploy/local/cli_validator_spec.rb | 22 ++++++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/lib/aws/codedeploy/local/cli_validator.rb b/lib/aws/codedeploy/local/cli_validator.rb index 254327c2..ef6e9e84 100644 --- a/lib/aws/codedeploy/local/cli_validator.rb +++ b/lib/aws/codedeploy/local/cli_validator.rb @@ -40,8 +40,12 @@ def validate(args) end if (type == 'directory' && (uri.scheme != 'https' && uri.scheme != 's3' && File.directory?(location))) - unless File.exists? "#{location}/appspec.yml" - raise ValidationError.new("Expecting appspec file at location #{location}/appspec.yml but it is not found there. Please either run the CLI from within a directory containing the appspec.yml file or specify a bundle location containing an appspec.yml file in its root directory") + appspec_filename = args['--appspec-filename'] + if !appspec_filename.nil? && !File.exists?("#{location}/#{appspec_filename}") + raise ValidationError.new("Expecting appspec file at location #{location}/#{appspec_filename} but it is not found there. Please either run the CLI from within a directory containing the #{appspec_filename} file or specify a bundle location containing an #{appspec_filename} file in its root directory") + end + if !File.exists?("#{location}/appspec.yml") && !File.exists?("#{location}/appspec.yaml") + raise ValidationError.new("Expecting appspec file at location #{location}/appspec.yml or #{location}/appspec.yaml but it is not found there. Please either run the CLI from within a directory containing the appspec.yml or appspec.yaml file or specify a bundle location containing an appspec.yml or appspec.yaml file in its root directory") end end @@ -60,7 +64,7 @@ def validate(args) end def any_new_revision_event_or_install_before_download_bundle(events) - events_using_new_revision.push('Install').any? do |event_not_allowed_before_download_bundle| + events_using_new_revision.push('Install').any? do |event_not_allowed_before_download_bundle| events.take_while{|e| e != 'DownloadBundle'}.include? event_not_allowed_before_download_bundle end end diff --git a/spec/aws/codedeploy/local/cli_validator_spec.rb b/spec/aws/codedeploy/local/cli_validator_spec.rb index 8f1b1f00..f3a54e68 100644 --- a/spec/aws/codedeploy/local/cli_validator_spec.rb +++ b/spec/aws/codedeploy/local/cli_validator_spec.rb @@ -86,7 +86,7 @@ end end - context 'when loction is directory but appspec is missing' do + context 'when location is directory but appspec is missing' do let(:args) do {"--bundle-location"=>FAKE_DIRECTORY, "--type"=>'directory'} @@ -96,7 +96,23 @@ 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(false) - expect{validator.validate(args)}.to raise_error(AWS::CodeDeploy::Local::CLIValidator::ValidationError, "Expecting appspec file at location #{FAKE_DIRECTORY}/appspec.yml but it is not found there. Please either run the CLI from within a directory containing the appspec.yml file or specify a bundle location containing an appspec.yml file in its root directory") + expect(File).to receive(:exists?).with("#{FAKE_DIRECTORY}/appspec.yaml").and_return(false) + expect{validator.validate(args)}.to raise_error(AWS::CodeDeploy::Local::CLIValidator::ValidationError, "Expecting appspec file at location #{FAKE_DIRECTORY}/appspec.yml or #{FAKE_DIRECTORY}/appspec.yaml but it is not found there. Please either run the CLI from within a directory containing the appspec.yml or appspec.yaml file or specify a bundle location containing an appspec.yml or appspec.yaml file in its root directory") + end + end + + context 'when location is directory and --appspec-filename is specified (but not existing)' do + let(:args) do + {"--bundle-location"=>FAKE_DIRECTORY, + "--type"=>'directory', + "--appspec-filename"=>"appspec-override.yaml"} + end + + it 'throws a ValidationError' 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-override.yaml").and_return(false) + expect{validator.validate(args)}.to raise_error(AWS::CodeDeploy::Local::CLIValidator::ValidationError, "Expecting appspec file at location #{FAKE_DIRECTORY}/appspec-override.yaml but it is not found there. Please either run the CLI from within a directory containing the appspec-override.yaml file or specify a bundle location containing an appspec-override.yaml file in its root directory") end end @@ -225,7 +241,7 @@ 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 end From 2f74f7d2c695d2315b617a59c6066f6507dce45e Mon Sep 17 00:00:00 2001 From: Chris Dibble Date: Mon, 22 Feb 2021 11:27:08 -0500 Subject: [PATCH 2/7] IMDS service is no-longer assumed to be available IMDS provides a wide range of host and data properties that can be used to setup and run the service. However, it is not always available due to security constraints or as the agent is running from an on-prem host. This updates allows for the situation where IMDS is not available and provides reasonable fallbacks. --- .ruby-version | 1 + bin/codedeploy-local | 4 + .../codedeploy-local/codedeploy_local.feature | 14 +++ .../appspec_override.yaml | 21 ++++ .../codedeploy_local_steps.rb | 7 +- features/step_definitions/common_steps.rb | 3 +- lib/aws/codedeploy/local/cli_validator.rb | 2 +- .../plugins/codedeploy/command_executor.rb | 96 ++++++++++--------- .../codedeploy/deployment_specification.rb | 2 +- 9 files changed, 99 insertions(+), 51 deletions(-) create mode 100644 .ruby-version create mode 100644 features/resources/sample_app_bundle_linux/appspec_override.yaml diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..bc4abe86 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.3.8 diff --git a/bin/codedeploy-local b/bin/codedeploy-local index 82519467..b8fc2ad4 100755 --- a/bin/codedeploy-local +++ b/bin/codedeploy-local @@ -54,6 +54,7 @@ Synopsis [--application-name ] [--events ] [--agent-configuration-file ] + [--appspec-filename ] Options ******* @@ -83,6 +84,9 @@ Options -c, --agent-configuration-file The location of a configuration file to use for the deployment, if you store it in a location other than the default. A configuration file specifies alternatives to other default values and behaviors for a deployment. By default, configuration files are stored as /etc/codedeploy-agent/conf/codedeployagent.yml (Amazon Linux, RHEL, or Ubuntu Server instances) or C:/ProgramData/Amazon/CodeDeploy/conf.yml (Windows Server). + -A, --appspec-filename + The name of the appspec file to use for the deployment, if you set it to a name other than the default. By default, appspec files are named "appspec.yml" or "appspec.yaml". + -h, --help Displays a summary of help content. diff --git a/features/codedeploy-local/codedeploy_local.feature b/features/codedeploy-local/codedeploy_local.feature index 067f349e..70a1b190 100644 --- a/features/codedeploy-local/codedeploy_local.feature +++ b/features/codedeploy-local/codedeploy_local.feature @@ -93,3 +93,17 @@ Feature: Local Deploy using AWS CodeDeploy Local CLI When I create a local deployment with my bundle with file-exists-behavior RETAIN Then the local deployment command should succeed And the expected existing file should end up like file-exists-behavior RETAIN specifies + + Scenario: Doing a sample local deployment using a directory bundle with a custom appspec filename + Given I have a sample local directory bundle + And I have a custom appspec filename appspec_override.yaml + When I create a local deployment with my bundle + Then the local deployment command should succeed + And the expected files should have have been locally deployed to my host + And the scripts should have been executed during local deployment + + Scenario: Doing a sample local deployment using a directory bundle with a non-existent custom appspec filename + Given I have a sample local directory bundle + And I have a custom appspec filename appspec_nonexistent.yaml + When I create a local deployment with my bundle + Then the local deployment command should fail diff --git a/features/resources/sample_app_bundle_linux/appspec_override.yaml b/features/resources/sample_app_bundle_linux/appspec_override.yaml new file mode 100644 index 00000000..ba69a0a3 --- /dev/null +++ b/features/resources/sample_app_bundle_linux/appspec_override.yaml @@ -0,0 +1,21 @@ +version: 0.0 +os: linux +hooks: + BeforeBlockTraffic: + - location: scripts/before_block_traffic.sh + AfterBlockTraffic: + - location: scripts/after_block_traffic.sh + ApplicationStop: + - location: scripts/application_stop.sh + BeforeInstall: + - location: scripts/before_install.sh + AfterInstall: + - location: scripts/after_install.sh + ApplicationStart: + - location: scripts/application_start.sh + ValidateService: + - location: scripts/validate_service.sh + BeforeAllowTraffic: + - location: scripts/before_allow_traffic.sh + AfterAllowTraffic: + - location: scripts/after_allow_traffic.sh diff --git a/features/step_definitions/codedeploy_local_steps.rb b/features/step_definitions/codedeploy_local_steps.rb index 0985ef2b..cac0ae1b 100644 --- a/features/step_definitions/codedeploy_local_steps.rb +++ b/features/step_definitions/codedeploy_local_steps.rb @@ -15,6 +15,7 @@ Before("@codedeploy-local") do @test_directory = Dir.mktmpdir configure_local_agent(@test_directory) + @appspec_filename = 'appspec.yml' end After("@codedeploy-local") do @@ -121,6 +122,10 @@ def tgz_app_bundle(temp_directory_to_create_bundle) tgz_file_name end +Given(/^I have a custom appspec filename ([^"]*)$/) do |custom_appspec_filename| + @appspec_filename = custom_appspec_filename +end + When(/^I create a local deployment with my bundle with only events (.+)$/) do |custom_events| @local_deployment_succeeded = create_local_deployment(custom_events.split(' ')) end @@ -148,7 +153,7 @@ def create_local_deployment(custom_events = nil, file_exists_behavior = nil) # 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}" + 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} --appspec-filename #{@appspec_filename}" end Then(/^the local deployment command should succeed$/) do diff --git a/features/step_definitions/common_steps.rb b/features/step_definitions/common_steps.rb index 2392309a..9d73d053 100644 --- a/features/step_definitions/common_steps.rb +++ b/features/step_definitions/common_steps.rb @@ -78,7 +78,8 @@ def write_zip_entries(entries, path, input_dir, zip_io) expect(files_and_directories_in_deployment_id_folder).to include(*%w(logs 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) + # most sample apps contain 2 files that should be present, except the linux sample app which contains an additional appspec file with a custom filename + expect(files_and_directories_in_deployment_archive_folder.size).to be_between(2, 3) expect(files_and_directories_in_deployment_archive_folder).to include(*%w(appspec.yml scripts)) 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") diff --git a/lib/aws/codedeploy/local/cli_validator.rb b/lib/aws/codedeploy/local/cli_validator.rb index ef6e9e84..bdf06d5d 100644 --- a/lib/aws/codedeploy/local/cli_validator.rb +++ b/lib/aws/codedeploy/local/cli_validator.rb @@ -44,7 +44,7 @@ def validate(args) if !appspec_filename.nil? && !File.exists?("#{location}/#{appspec_filename}") raise ValidationError.new("Expecting appspec file at location #{location}/#{appspec_filename} but it is not found there. Please either run the CLI from within a directory containing the #{appspec_filename} file or specify a bundle location containing an #{appspec_filename} file in its root directory") end - if !File.exists?("#{location}/appspec.yml") && !File.exists?("#{location}/appspec.yaml") + if appspec_filename.nil? && !File.exists?("#{location}/appspec.yml") && !File.exists?("#{location}/appspec.yaml") raise ValidationError.new("Expecting appspec file at location #{location}/appspec.yml or #{location}/appspec.yaml but it is not found there. Please either run the CLI from within a directory containing the appspec.yml or appspec.yaml file or specify a bundle location containing an appspec.yml or appspec.yaml file in its root directory") end end diff --git a/lib/instance_agent/plugins/codedeploy/command_executor.rb b/lib/instance_agent/plugins/codedeploy/command_executor.rb index 687b7eff..0a2fbb0a 100644 --- a/lib/instance_agent/plugins/codedeploy/command_executor.rb +++ b/lib/instance_agent/plugins/codedeploy/command_executor.rb @@ -1,6 +1,7 @@ require 'openssl' require 'fileutils' require 'aws-sdk-core' +require 'aws-sdk-s3' require 'zlib' require 'zip' require 'instance_metadata' @@ -12,6 +13,7 @@ require 'instance_agent/plugins/codedeploy/deployment_specification' require 'instance_agent/plugins/codedeploy/hook_executor' require 'instance_agent/plugins/codedeploy/installer' +require 'instance_agent/string_utils' module InstanceAgent module Plugins @@ -47,8 +49,8 @@ def initialize(options = {}) def self.command(name, &blk) @command_methods ||= Hash.new - - method = Seahorse::Util.underscore(name).to_sym + raise "Received command is not in PascalCase form: #{name.to_s}" unless StringUtils.is_pascal_case(name.to_s) + method = StringUtils.underscore(name.to_s) @command_methods[name] = method define_method(method, &blk) @@ -154,7 +156,7 @@ def map :deployment_root_dir => deployment_root_dir(deployment_spec), :last_successful_deployment_dir => last_successful_deployment_dir(deployment_spec.deployment_group_id), :most_recent_deployment_dir => most_recent_deployment_dir(deployment_spec.deployment_group_id), - :app_spec_path => deployment_spec.app_spec_path) + :app_spec_path => app_spec_path) script_log.concat_log(hook_command.execute) end script_log.log @@ -183,7 +185,7 @@ def last_successful_deployment_dir(deployment_group) return unless File.exist? last_successful_install_file_location File.open last_successful_install_file_location do |f| return f.read.chomp - end + end end private @@ -192,30 +194,29 @@ def most_recent_deployment_dir(deployment_group) return unless File.exist? most_recent_install_file_location File.open most_recent_install_file_location do |f| return f.read.chomp - end + end end private def default_app_spec(deployment_spec) - app_spec_location = app_spec_real_path(deployment_spec) - validate_app_spec_hooks(app_spec_location, deployment_spec.all_possible_lifecycle_events) + default_app_spec_location = File.join(archive_root_dir(deployment_spec), app_spec_path) + log(:debug, "Checking for app spec in #{default_app_spec_location}") + validate_app_spec_hooks(ApplicationSpecification::ApplicationSpecification.parse(File.read(default_app_spec_location)), deployment_spec.all_possible_lifecycle_events) end private - def validate_app_spec_hooks(app_spec_location, all_possible_lifecycle_events) - app_spec = ApplicationSpecification::ApplicationSpecification.parse(File.read(app_spec_location)) - app_spec_filename = File.basename(app_spec_location) + def validate_app_spec_hooks(app_spec, all_possible_lifecycle_events) unless all_possible_lifecycle_events.nil? app_spec_hooks_plus_hooks_from_mapping = app_spec.hooks.keys.to_set.merge(@hook_mapping.keys).to_a unless app_spec_hooks_plus_hooks_from_mapping.to_set.subset?(all_possible_lifecycle_events.to_set) unknown_lifecycle_events = app_spec_hooks_plus_hooks_from_mapping - all_possible_lifecycle_events - raise ArgumentError.new("#{app_spec_filename} file contains unknown lifecycle events: #{unknown_lifecycle_events}") + raise ArgumentError.new("appspec.yml file contains unknown lifecycle events: #{unknown_lifecycle_events}") end app_spec_hooks_plus_hooks_from_default_mapping = app_spec.hooks.keys.to_set.merge(InstanceAgent::Plugins::CodeDeployPlugin::CommandPoller::DEFAULT_HOOK_MAPPING.keys).to_a custom_hooks_not_found_in_appspec = custom_lifecycle_events(all_possible_lifecycle_events) - app_spec_hooks_plus_hooks_from_default_mapping unless (custom_hooks_not_found_in_appspec).empty? - raise ArgumentError.new("You specified a lifecycle event which is not a default one and doesn't exist in your #{app_spec_filename} file: #{custom_hooks_not_found_in_appspec.join(',')}") + raise ArgumentError.new("You specified a lifecycle event which is not a default one and doesn't exist in your appspec.yml file: #{custom_hooks_not_found_in_appspec.join(',')}") end end @@ -238,17 +239,32 @@ def most_recent_install_file_path(deployment_group) private def download_from_s3(deployment_spec, bucket, key, version, etag) - log(:debug, "Downloading artifact bundle from bucket '#{bucket}' and key '#{key}', version '#{version}', etag '#{etag}'") - s3 = Aws::S3::Client.new(s3_options) + log(:info, "Downloading artifact bundle from bucket '#{bucket}' and key '#{key}', version '#{version}', etag '#{etag}'") + options = s3_options() + s3 = Aws::S3::Client.new(options) + ProcessManager::Log.info("s3 client configuration below:") + ProcessManager::Log.info(s3.config) File.open(artifact_bundle(deployment_spec), 'wb') do |file| + begin if !version.nil? object = s3.get_object({:bucket => bucket, :key => key, :version_id => version}, :target => file) else object = s3.get_object({:bucket => bucket, :key => key}, :target => file) end + rescue Seahorse::Client::NetworkingError => e + if e.message.include? "unable to connect to" + if InstanceAgent::Config.config[:use_fips_mode] + raise $!, "#{$!}. Check that Fips exists in #{options[:region]}. Or, try using s3 endpoint override.", $!.backtrace + else + raise $!, "#{$!}. Try using s3 endpoint override.", $!.backtrace + end + else + raise + end + end if(!etag.nil? && !(etag.gsub(/"/,'').eql? object.etag.gsub(/"/,''))) msg = "Expected deployment artifact bundle etag #{etag} but was actually #{object.etag}" @@ -256,7 +272,7 @@ def download_from_s3(deployment_spec, bucket, key, version, etag) raise RuntimeError, msg end end - log(:debug, "Download complete from bucket #{bucket} and key #{key}") + log(:info, "Download complete from bucket #{bucket} and key #{key}") end public @@ -267,17 +283,19 @@ def s3_options region = ENV['AWS_REGION'] || InstanceMetadata.region options[:region] = region + if !InstanceAgent::Config.config[:s3_endpoint_override].to_s.empty? + ProcessManager::Log.info("using s3 override endpoint #{InstanceAgent::Config.config[:s3_endpoint_override]}") options[:endpoint] = URI(InstanceAgent::Config.config[:s3_endpoint_override]) elsif InstanceAgent::Config.config[:use_fips_mode] - #S3 Fips pseudo-regions are not supported by the SDK yet - #source for the URL: https://aws.amazon.com/compliance/fips/ - options[:endpoint] = "https://s3-fips.#{region}.amazonaws.com" + ProcessManager::Log.info("using fips endpoint") + # This is not a true region but a way to signal to the S3 client that a FIPS enpoint should be used; added in SDK3. + options[:region] = "fips-#{region}" end proxy_uri = nil if InstanceAgent::Config.config[:proxy_uri] proxy_uri = URI(InstanceAgent::Config.config[:proxy_uri]) - end + end options[:http_proxy] = proxy_uri if InstanceAgent::Config.config[:log_aws_wire] @@ -289,10 +307,10 @@ def s3_options 64 * 1024 * 1024) options[:http_wire_trace] = true end - - options - end - + + options + end + private def download_from_github(deployment_spec, account, repo, commit, anonymous, token) @@ -344,7 +362,7 @@ def download_from_github(deployment_spec, account, repo, commit, anonymous, toke if retries < 3 time_to_sleep = (10 * (3 ** retries)) # 10 sec, 30 sec, 90 sec - log(:debug, "Retrying download in #{time_to_sleep} seconds.") + log(:info, "Retrying download in #{time_to_sleep} seconds.") sleep(time_to_sleep) retries += 1 retry @@ -358,6 +376,7 @@ def download_from_github(deployment_spec, account, repo, commit, anonymous, toke def handle_local_file(deployment_spec, local_location) # Symlink local file to the location where download is expected to go bundle_file = artifact_bundle(deployment_spec) + log(:info, "Handle local file #{bundle_file}") begin File.symlink local_location, bundle_file rescue @@ -370,6 +389,7 @@ def handle_local_file(deployment_spec, local_location) def handle_local_directory(deployment_spec, local_location) # Copy local directory to the location where a file would have been extracted # We copy instead of symlinking in order to preserve revision history + log(:info, "Handle local directory #{local_location}") FileUtils.cp_r local_location, archive_root_dir(deployment_spec) end @@ -403,7 +423,7 @@ def unpack_bundle(cmd, bundle_file, deployment_spec) # If the top level of the archive is a directory that contains an appspec, # strip that before giving up - if ((archive_root_files.size == 1) && + if ((archive_root_files.size == 1) && File.directory?(File.join(dst, archive_root_files[0])) && Dir.entries(File.join(dst, archive_root_files[0])).grep(/appspec/i).any?) log(:info, "Stripping leading directory from archive bundle contents.") @@ -418,7 +438,7 @@ def unpack_bundle(cmd, bundle_file, deployment_spec) FileUtils.mv(nested_archive_root, dst) FileUtils.rmdir(tmp_dst) - log(:debug, Dir.entries(dst).join("; ")) + log(:debug, Dir.entries(dst).join("; ")) end end @@ -434,7 +454,7 @@ def update_most_recent_install(deployment_spec) File.open(most_recent_install_file_path(deployment_spec.deployment_group_id), 'w+') do |f| f.write deployment_root_dir(deployment_spec) end - end + end private def cleanup_old_archives(deployment_spec) @@ -446,7 +466,7 @@ def cleanup_old_archives(deployment_spec) full_path_deployment_archives = deployment_archives.map{ |f| File.join(ProcessManager::Config.config[:root_dir], deployment_group, f)} full_path_deployment_archives.delete(deployment_root_dir(deployment_spec)) - + extra = full_path_deployment_archives.size - @archives_to_retain + 1 return unless extra > 0 @@ -459,7 +479,7 @@ def cleanup_old_archives(deployment_spec) # Absolute path takes care of relative root directories directories = oldest_extra.map{ |f| File.absolute_path(f) } - log(:debug, "Delete Files #{directories}") + log(:debug,"Delete Files #{directories}" ) InstanceAgent::Platform.util.delete_dirs_command(directories) end @@ -474,24 +494,6 @@ def app_spec_path 'appspec.yml' end - # Checks for existence the possible extensions of the app_spec_path (.yml and .yaml) - private - def app_spec_real_path(deployment_spec) - app_spec_param_location = File.join(archive_root_dir(deployment_spec), deployment_spec.app_spec_path) - app_spec_yaml_location = File.join(archive_root_dir(deployment_spec), "appspec.yaml") - app_spec_yml_location = File.join(archive_root_dir(deployment_spec), "appspec.yml") - if File.exist? app_spec_param_location - log(:debug, "Using appspec file #{app_spec_param_location}") - app_spec_param_location - elsif File.exist? app_spec_yaml_location - log(:debug, "Using appspec file #{app_spec_yaml_location}") - app_spec_yaml_location - else - log(:debug, "Using appspec file #{app_spec_yml_location}") - app_spec_yml_location - end - end - private def description self.class.to_s diff --git a/lib/instance_agent/plugins/codedeploy/deployment_specification.rb b/lib/instance_agent/plugins/codedeploy/deployment_specification.rb index 664f9662..54fdfbe7 100644 --- a/lib/instance_agent/plugins/codedeploy/deployment_specification.rb +++ b/lib/instance_agent/plugins/codedeploy/deployment_specification.rb @@ -175,4 +175,4 @@ def getDeploymentIdFromArn(arn) end end end -end +end \ No newline at end of file From 91590f289063edd80424e60713c54b1d2f639fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Johnson=20=F0=9F=9A=80?= Date: Tue, 15 Dec 2020 14:17:34 -0800 Subject: [PATCH 3/7] CDVSA-185 making appspec filename configurable and support yml and yaml extensions --- .../plugins/codedeploy/command_executor.rb | 67 +- .../codedeploy/command_executor_test.rb | 58 +- .../codedeploy/command_executor_test.rb.orig | 873 ++++++++++++++++++ 3 files changed, 954 insertions(+), 44 deletions(-) create mode 100644 test/instance_agent/plugins/codedeploy/command_executor_test.rb.orig diff --git a/lib/instance_agent/plugins/codedeploy/command_executor.rb b/lib/instance_agent/plugins/codedeploy/command_executor.rb index 0a2fbb0a..253f5076 100644 --- a/lib/instance_agent/plugins/codedeploy/command_executor.rb +++ b/lib/instance_agent/plugins/codedeploy/command_executor.rb @@ -156,7 +156,7 @@ def map :deployment_root_dir => deployment_root_dir(deployment_spec), :last_successful_deployment_dir => last_successful_deployment_dir(deployment_spec.deployment_group_id), :most_recent_deployment_dir => most_recent_deployment_dir(deployment_spec.deployment_group_id), - :app_spec_path => app_spec_path) + :app_spec_path => deployment_spec.app_spec_path) script_log.concat_log(hook_command.execute) end script_log.log @@ -185,7 +185,7 @@ def last_successful_deployment_dir(deployment_group) return unless File.exist? last_successful_install_file_location File.open last_successful_install_file_location do |f| return f.read.chomp - end + end end private @@ -194,29 +194,30 @@ def most_recent_deployment_dir(deployment_group) return unless File.exist? most_recent_install_file_location File.open most_recent_install_file_location do |f| return f.read.chomp - end + end end private def default_app_spec(deployment_spec) - default_app_spec_location = File.join(archive_root_dir(deployment_spec), app_spec_path) - log(:debug, "Checking for app spec in #{default_app_spec_location}") - validate_app_spec_hooks(ApplicationSpecification::ApplicationSpecification.parse(File.read(default_app_spec_location)), deployment_spec.all_possible_lifecycle_events) + app_spec_location = app_spec_real_path(deployment_spec) + validate_app_spec_hooks(app_spec_location, deployment_spec.all_possible_lifecycle_events) end private - def validate_app_spec_hooks(app_spec, all_possible_lifecycle_events) + def validate_app_spec_hooks(app_spec_location, all_possible_lifecycle_events) + app_spec = ApplicationSpecification::ApplicationSpecification.parse(File.read(app_spec_location)) + app_spec_filename = File.basename(app_spec_location) unless all_possible_lifecycle_events.nil? app_spec_hooks_plus_hooks_from_mapping = app_spec.hooks.keys.to_set.merge(@hook_mapping.keys).to_a unless app_spec_hooks_plus_hooks_from_mapping.to_set.subset?(all_possible_lifecycle_events.to_set) unknown_lifecycle_events = app_spec_hooks_plus_hooks_from_mapping - all_possible_lifecycle_events - raise ArgumentError.new("appspec.yml file contains unknown lifecycle events: #{unknown_lifecycle_events}") + raise ArgumentError.new("#{app_spec_filename} file contains unknown lifecycle events: #{unknown_lifecycle_events}") end app_spec_hooks_plus_hooks_from_default_mapping = app_spec.hooks.keys.to_set.merge(InstanceAgent::Plugins::CodeDeployPlugin::CommandPoller::DEFAULT_HOOK_MAPPING.keys).to_a custom_hooks_not_found_in_appspec = custom_lifecycle_events(all_possible_lifecycle_events) - app_spec_hooks_plus_hooks_from_default_mapping unless (custom_hooks_not_found_in_appspec).empty? - raise ArgumentError.new("You specified a lifecycle event which is not a default one and doesn't exist in your appspec.yml file: #{custom_hooks_not_found_in_appspec.join(',')}") + raise ArgumentError.new("You specified a lifecycle event which is not a default one and doesn't exist in your #{app_spec_filename} file: #{custom_hooks_not_found_in_appspec.join(',')}") end end @@ -239,7 +240,6 @@ def most_recent_install_file_path(deployment_group) private def download_from_s3(deployment_spec, bucket, key, version, etag) - log(:info, "Downloading artifact bundle from bucket '#{bucket}' and key '#{key}', version '#{version}', etag '#{etag}'") options = s3_options() s3 = Aws::S3::Client.new(options) @@ -289,13 +289,16 @@ def s3_options options[:endpoint] = URI(InstanceAgent::Config.config[:s3_endpoint_override]) elsif InstanceAgent::Config.config[:use_fips_mode] ProcessManager::Log.info("using fips endpoint") - # This is not a true region but a way to signal to the S3 client that a FIPS enpoint should be used; added in SDK3. - options[:region] = "fips-#{region}" + # There was a recent change to S3 client to decompose the region and use a FIPS endpoint is "fips-" is appended + # to the region. However, this is such a recent change that we cannot rely on the latest version of the SDK to be loaded. + # For now, the endpoint will be set directly if FIPS is active but can switch to the S3 method once we have broader support. + # options[:region] = "fips-#{region}" + options[:endpoint] = "https://s3-fips.#{region}.amazonaws.com" end proxy_uri = nil if InstanceAgent::Config.config[:proxy_uri] proxy_uri = URI(InstanceAgent::Config.config[:proxy_uri]) - end + end options[:http_proxy] = proxy_uri if InstanceAgent::Config.config[:log_aws_wire] @@ -307,10 +310,10 @@ def s3_options 64 * 1024 * 1024) options[:http_wire_trace] = true end - - options - end - + + options + end + private def download_from_github(deployment_spec, account, repo, commit, anonymous, token) @@ -423,7 +426,7 @@ def unpack_bundle(cmd, bundle_file, deployment_spec) # If the top level of the archive is a directory that contains an appspec, # strip that before giving up - if ((archive_root_files.size == 1) && + if ((archive_root_files.size == 1) && File.directory?(File.join(dst, archive_root_files[0])) && Dir.entries(File.join(dst, archive_root_files[0])).grep(/appspec/i).any?) log(:info, "Stripping leading directory from archive bundle contents.") @@ -438,7 +441,7 @@ def unpack_bundle(cmd, bundle_file, deployment_spec) FileUtils.mv(nested_archive_root, dst) FileUtils.rmdir(tmp_dst) - log(:debug, Dir.entries(dst).join("; ")) + log(:debug, Dir.entries(dst).join("; ")) end end @@ -454,7 +457,7 @@ def update_most_recent_install(deployment_spec) File.open(most_recent_install_file_path(deployment_spec.deployment_group_id), 'w+') do |f| f.write deployment_root_dir(deployment_spec) end - end + end private def cleanup_old_archives(deployment_spec) @@ -466,7 +469,7 @@ def cleanup_old_archives(deployment_spec) full_path_deployment_archives = deployment_archives.map{ |f| File.join(ProcessManager::Config.config[:root_dir], deployment_group, f)} full_path_deployment_archives.delete(deployment_root_dir(deployment_spec)) - + extra = full_path_deployment_archives.size - @archives_to_retain + 1 return unless extra > 0 @@ -479,7 +482,7 @@ def cleanup_old_archives(deployment_spec) # Absolute path takes care of relative root directories directories = oldest_extra.map{ |f| File.absolute_path(f) } - log(:debug,"Delete Files #{directories}" ) + log(:debug, "Delete Files #{directories}") InstanceAgent::Platform.util.delete_dirs_command(directories) end @@ -494,6 +497,24 @@ def app_spec_path 'appspec.yml' end + # Checks for existence the possible extensions of the app_spec_path (.yml and .yaml) + private + def app_spec_real_path(deployment_spec) + app_spec_param_location = File.join(archive_root_dir(deployment_spec), deployment_spec.app_spec_path) + app_spec_yaml_location = File.join(archive_root_dir(deployment_spec), "appspec.yaml") + app_spec_yml_location = File.join(archive_root_dir(deployment_spec), "appspec.yml") + if File.exist? app_spec_param_location + log(:debug, "Using appspec file #{app_spec_param_location}") + app_spec_param_location + elsif File.exist? app_spec_yaml_location + log(:debug, "Using appspec file #{app_spec_yaml_location}") + app_spec_yaml_location + else + log(:debug, "Using appspec file #{app_spec_yml_location}") + app_spec_yml_location + end + end + private def description self.class.to_s @@ -507,4 +528,4 @@ def log(severity, message) end end end -end +end \ No newline at end of file diff --git a/test/instance_agent/plugins/codedeploy/command_executor_test.rb b/test/instance_agent/plugins/codedeploy/command_executor_test.rb index e61898d8..9a44e2ed 100644 --- a/test/instance_agent/plugins/codedeploy/command_executor_test.rb +++ b/test/instance_agent/plugins/codedeploy/command_executor_test.rb @@ -1,7 +1,7 @@ require 'test_helper' require 'certificate_helper' require 'stringio' -require 'aws-sdk-core/s3' +require 'aws-sdk-s3' require 'aws/codedeploy/local/deployer' @@ -32,7 +32,11 @@ def generate_signed_message_for(map) :deploy_control_client => @deploy_control_client, :hook_mapping => @test_hook_mapping}) @aws_region = 'us-east-1' + @partition = 'aws' + @domain = 'amazonaws.com' InstanceMetadata.stubs(:region).returns(@aws_region) + InstanceMetadata.stubs(:partition).returns(@partition) + InstanceMetadata.stubs(:domain).returns(@domain) end context "deployment_system method" do @@ -332,6 +336,7 @@ def generate_signed_message_for(map) @mock_file.stubs(:close) @http.stubs(:request_get) @s3 = mock + @s3.stubs(:config).returns("hello") Aws::S3::Client.stubs(:new).returns(@s3) end @@ -401,33 +406,44 @@ def generate_signed_message_for(map) context "when creating S3 options" do - should "use right region" do - assert_equal 'us-east-1', @command_executor.s3_options[:region] - end - +<<<<<<< HEAD should "use right signature version" do +======= + should "use right signature version" do +>>>>>>> 3515520... CDVSA-185 making appspec filename configurable and support yml and yaml extensions assert_equal 'v4', @command_executor.s3_options[:signature_version] end - should "use right endpoint when using Fips" do - InstanceAgent::Config.config[:use_fips_mode] = true - assert_equal 'https://s3-fips.us-east-1.amazonaws.com', @command_executor.s3_options[:endpoint] - InstanceAgent::Config.config[:use_fips_mode] = false + context "when override endpoint provided" do + setup do + InstanceAgent::Config.config[:s3_endpoint_override] = "https://example.override.endpoint.com" + end + should "use the override endpoint" do + assert_equal "https://example.override.endpoint.com", @command_executor.s3_options[:endpoint].to_s + end end - should "use right endpoint when using endpoint override" do - s3_endpoint_override_url = 'htpp://testendpointoverride' - InstanceAgent::Config.config[:s3_endpoint_override] = s3_endpoint_override_url - assert_equal s3_endpoint_override_url, @command_executor.s3_options[:endpoint].to_s - InstanceAgent::Config.config[:s3_endpoint_override] = nil + context "when no override endpoint provided and not using fips" do + setup do + InstanceAgent::Config.config[:s3_endpoint_override] = nil + InstanceAgent::Config.config[:use_fips_mode] = false + end + should "use correct region and custom endpoint" do + assert_equal 'us-east-1', @command_executor.s3_options[:region] + assert_false @command_executor.s3_options.include? :endpoint + end end - should "use no endpoint when neither using Fips nor Endpoint override" do - InstanceAgent::Config.config[:s3_endpoint_override] = nil - InstanceAgent::Config.config[:use_fips_mode] = false - assert_false @command_executor.s3_options.include? :endpoint + context "when no override endpoint provided and using fips" do + setup do + InstanceAgent::Config.config[:s3_endpoint_override] = nil + InstanceAgent::Config.config[:use_fips_mode] = true + end + should "use correct region and custom endpoint" do + assert_equal 'us-east-1', @command_executor.s3_options[:region] + assert_true @command_executor.s3_options.include? :endpoint + end end - end context "downloading bundle from S3" do @@ -809,8 +825,8 @@ def generate_signed_message_for(map) #non 1:1 mapping tests context "one command hooks to multiple lifecycle events" do setup do - @command.command_name = "test_command" - @test_hook_mapping = { "test_command" => ["lifecycle_event_1","lifecycle_event_2"]} + @command.command_name = "TestCommand" + @test_hook_mapping = { "TestCommand" => ["lifecycle_event_1","lifecycle_event_2"]} @deploy_control_client = mock @command_executor = InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor.new({ :deploy_control_client => @deploy_control_client, diff --git a/test/instance_agent/plugins/codedeploy/command_executor_test.rb.orig b/test/instance_agent/plugins/codedeploy/command_executor_test.rb.orig new file mode 100644 index 00000000..9a44e2ed --- /dev/null +++ b/test/instance_agent/plugins/codedeploy/command_executor_test.rb.orig @@ -0,0 +1,873 @@ +require 'test_helper' +require 'certificate_helper' +require 'stringio' +require 'aws-sdk-s3' + +require 'aws/codedeploy/local/deployer' + +class CodeDeployPluginCommandExecutorTest < InstanceAgentTestCase + + include InstanceAgent::Plugins::CodeDeployPlugin + def generate_signed_message_for(map) + message = @cert_helper.sign_message(map.to_json) + spec = OpenStruct.new({ :payload => message }) + spec.format = "PKCS7/JSON" + + return spec + end + + context 'The CodeDeploy Plugin Command Executor' do + setup do + @test_hook_mapping = { "BeforeBlockTraffic"=>["BeforeBlockTraffic"], + "AfterBlockTraffic"=>["AfterBlockTraffic"], + "ApplicationStop"=>["ApplicationStop"], + "BeforeInstall"=>["BeforeInstall"], + "AfterInstall"=>["AfterInstall"], + "ApplicationStart"=>["ApplicationStart"], + "BeforeAllowTraffic"=>["BeforeAllowTraffic"], + "AfterAllowTraffic"=>["AfterAllowTraffic"], + "ValidateService"=>["ValidateService"]} + @deploy_control_client = mock + @command_executor = InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor.new({ + :deploy_control_client => @deploy_control_client, + :hook_mapping => @test_hook_mapping}) + @aws_region = 'us-east-1' + @partition = 'aws' + @domain = 'amazonaws.com' + InstanceMetadata.stubs(:region).returns(@aws_region) + InstanceMetadata.stubs(:partition).returns(@partition) + InstanceMetadata.stubs(:domain).returns(@domain) + end + + context "deployment_system method" do + should "always return CodeDeploy" do + assert_equal "CodeDeploy", @command_executor.deployment_system + end + end + + context "when executing a command" do + + setup do + @cert_helper = CertificateHelper.new + @deployment_id = SecureRandom.uuid + @deployment_group_name = "TestDeploymentGroup" + @application_name = "TestApplicationName" + @deployment_group_id = "foo" + @deployment_creator = "User" + @deployment_type = "IN_PLACE" + @s3Revision = { + "Bucket" => "mybucket", + "Key" => "mykey", + "BundleType" => "tar" + } + @file_exists_behavior = "RETAIN" + @agent_actions_overrides_map = {"FileExistsBehavior" => @file_exists_behavior} + @agent_actions_overrides = {"AgentOverrides" => @agent_actions_overrides_map} + @deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "DeploymentCreator" => @deployment_creator, + "DeploymentType" => @deployment_type, + "AgentActionOverrides" => @agent_actions_overrides, + "Revision" => { + "RevisionType" => "S3", + "S3Revision" => @s3Revision + } + }) + @command = Aws::CodeDeployCommand::Types::HostCommandInstance.new( + :host_command_identifier => "command-1", + :deployment_execution_id => "test-execution") + @root_dir = '/tmp/codedeploy/' + @deployment_root_dir = File.join(@root_dir, @deployment_group_id.to_s, @deployment_id.to_s) + @deployment_instructions_dir = File.join(@root_dir, 'deployment-instructions') + @archive_root_dir = File.join(@deployment_root_dir, 'deployment-archive') + ProcessManager::Config.config[:root_dir] = @root_dir + + FileUtils.stubs(:mkdir_p) + File.stubs(:directory?).with(@deployment_root_dir).returns(true) + @last_successful_install_file_location = File.join(@deployment_instructions_dir, "#{@deployment_group_id}_last_successful_install") + @most_recent_install_file_location = File.join(@deployment_instructions_dir, "#{@deployment_group_id}_most_recent_install") + end + + context "when executing an unknown command" do + setup do + @command.command_name = "unknown-command" + end + + should "not create the deployment root directory" do + # Need to unstub the :mkdir_p method otherwise the never expectation doesn't work + FileUtils.unstub(:mkdir_p) + FileUtils.expects(:mkdir_p).never + + assert_raised_with_message('Unsupported command type: unknown-command.', InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor::InvalidCommandNameFailure) do + @command_executor.execute_command(@command, @deployment_spec) + end + end + + should "throw an exception" do + assert_raised_with_message('Unsupported command type: unknown-command.', InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor::InvalidCommandNameFailure) do + @command_executor.execute_command(@command, @deployment_spec) + end + end + end + + context "when executing a valid command" do + setup do + @command.command_name = "Install" + @command_executor.stubs(:install) + end + + should "create the deployment root directory" do + FileUtils.expects(:mkdir_p).with(@deployment_root_dir) + + @command_executor.execute_command(@command, @deployment_spec) + end + + context "when failed to create root directory" do + setup do + File.stubs(:directory?).with(@deployment_root_dir).returns(false) + end + + should "raise an exception" do + assert_raised_with_message("Error creating deployment root directory #{@deployment_root_dir}") do + @command_executor.execute_command(@command, @deployment_spec) + end + end + end + end + + context "when executing the Install command" do + + setup do + @command.command_name = "Install" + InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::ApplicationSpecification.stubs(:parse).returns(@app_spec) + @installer = stub("installer", :install => nil) + Installer.stubs(:new).returns(@installer) + File.stubs(:directory?).with(@deployment_instructions_dir).returns(true) + File.stubs(:exist?).with(@last_successful_install_file_location).returns(true) + File.stubs(:exist?).with(@archive_root_dir).returns(true) + File.stubs(:open).with(@last_successful_install_file_location, 'w+') + File.stubs(:open).with(@last_successful_install_file_location) + + @app_spec = mock("parsed application specification") + File.stubs(:exist?).with("#@archive_root_dir/appspec.yml").returns(true) + File.stubs(:read).with("#@archive_root_dir/appspec.yml").returns("APP SPEC") + ApplicationSpecification::ApplicationSpecification.stubs(:parse).with("APP SPEC").returns(@app_spec) + end + + should "create an appropriate Installer" do + Installer. + expects(:new). + with(:deployment_instructions_dir => @deployment_instructions_dir, + :deployment_archive_dir => @archive_root_dir, + :file_exists_behavior => @file_exists_behavior). + returns(@installer) + + @command_executor.execute_command(@command, @deployment_spec) + end + + should "perform the installation for the current IG, revision and app spec" do + @installer.expects(:install).with(@deployment_group_id, @app_spec) + + @command_executor.execute_command(@command, @deployment_spec) + end + + should "write the archive root dir to the install instructions file" do + mock_file = mock + File.expects(:open).with(@last_successful_install_file_location, 'w+').yields(mock_file) + mock_file.expects(:write).with(@deployment_root_dir) + + @command_executor.execute_command(@command, @deployment_spec) + end + + should 'raise ArgumentError if appspec contains unknown hook and deployment_spec includes all_possible_lifecycle_events' do + all_possible_lifecycle_events = ['ExampleLifecycleEvent', 'SecondLifecycleEvent'] + deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "DeploymentCreator" => @deployment_creator, + "DeploymentType" => @deployment_type, + "AgentActionOverrides" => @agent_actions_overrides, + "AllPossibleLifecycleEvents" => all_possible_lifecycle_events, + "Revision" => { + "RevisionType" => "S3", + "S3Revision" => @s3Revision + } + }) + + app_spec = mock("parsed application specification") + app_spec_hooks = {'UnknownHook' => nil} + app_spec.expects(:hooks).returns(app_spec_hooks) + File.stubs(:read).with("#@archive_root_dir/appspec.yml").returns("APP SPEC") + ApplicationSpecification::ApplicationSpecification.stubs(:parse).with("APP SPEC").returns(app_spec) + unknown_hooks = app_spec_hooks.merge(@test_hook_mapping) + assert_raised_with_message("appspec.yml file contains unknown lifecycle events: #{unknown_hooks.keys}", ArgumentError) do + @command_executor.execute_command(@command, deployment_spec) + end + end + + should 'raise ArgumentError if appspec custom hook specified that does not exist in appspec' do + all_possible_lifecycle_events = AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS + ['ExampleLifecycleEvent', 'SecondLifecycleEvent', 'CustomHookNotInAppspec'] + deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "DeploymentCreator" => @deployment_creator, + "DeploymentType" => @deployment_type, + "AgentActionOverrides" => @agent_actions_overrides, + "AllPossibleLifecycleEvents" => all_possible_lifecycle_events, + "Revision" => { + "RevisionType" => "S3", + "S3Revision" => @s3Revision + } + }) + + app_spec = mock("parsed application specification") + app_spec_hooks = InstanceAgent::Plugins::CodeDeployPlugin::CommandPoller::DEFAULT_HOOK_MAPPING.merge({'ExampleLifecycleEvent' => nil, 'SecondLifecycleEvent' => nil}) + app_spec.expects(:hooks).twice.returns(app_spec_hooks) + File.stubs(:read).with("#@archive_root_dir/appspec.yml").returns("APP SPEC") + ApplicationSpecification::ApplicationSpecification.stubs(:parse).with("APP SPEC").returns(app_spec) + assert_raised_with_message("You specified a lifecycle event which is not a default one and doesn't exist in your appspec.yml file: CustomHookNotInAppspec", ArgumentError) do + @command_executor.execute_command(@command, deployment_spec) + end + end + + should 'honor the AppSpecFilename command variable' do + deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "DeploymentCreator" => @deployment_creator, + "DeploymentType" => @deployment_type, + "AgentActionOverrides" => @agent_actions_overrides, + "AppSpecFilename" => "appspec-override.yaml", + "Revision" => { + "RevisionType" => "S3", + "S3Revision" => @s3Revision + } + }) + + File.expects(:exist?).with("#@archive_root_dir/appspec-override.yaml").returns(true) + File.expects(:read).with("#@archive_root_dir/appspec-override.yaml").returns("APP SPEC") + @command_executor.execute_command(@command, deployment_spec) + end + + should 'fallback to appspec.yaml if provided AppSpecFilename variable points to a file which does\'t exist' do + deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "DeploymentCreator" => @deployment_creator, + "DeploymentType" => @deployment_type, + "AgentActionOverrides" => @agent_actions_overrides, + "AppSpecFilename" => "appspec-override.yaml", + "Revision" => { + "RevisionType" => "S3", + "S3Revision" => @s3Revision + } + }) + + File.expects(:exist?).with("#@archive_root_dir/appspec-override.yaml").returns(false) + File.expects(:exist?).with("#@archive_root_dir/appspec.yaml").returns(true) + File.expects(:read).with("#@archive_root_dir/appspec.yaml").returns("APP SPEC") + @command_executor.execute_command(@command, deployment_spec) + end + should 'fallback to appspec.yaml if both AppSpecFilename variable and appspec.yaml don\'t exist' do + deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "DeploymentCreator" => @deployment_creator, + "DeploymentType" => @deployment_type, + "AgentActionOverrides" => @agent_actions_overrides, + "AppSpecFilename" => "appspec-override.yaml", + "Revision" => { + "RevisionType" => "S3", + "S3Revision" => @s3Revision + } + }) + + File.expects(:exist?).with("#@archive_root_dir/appspec-override.yaml").returns(false) + File.expects(:exist?).with("#@archive_root_dir/appspec.yaml").returns(false) + File.expects(:read).with("#@archive_root_dir/appspec.yml").returns("APP SPEC") + @command_executor.execute_command(@command, deployment_spec) + end + + should 'fallback to appspec.yaml if appspec.yml is not there and no AppSpecFilename arg is specified' do + deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "DeploymentCreator" => @deployment_creator, + "DeploymentType" => @deployment_type, + "AgentActionOverrides" => @agent_actions_overrides, + "Revision" => { + "RevisionType" => "S3", + "S3Revision" => @s3Revision + } + }) + + File.expects(:exist?).with("#@archive_root_dir/appspec.yml").returns(false) + File.expects(:exist?).with("#@archive_root_dir/appspec.yaml").returns(true) + File.expects(:read).with("#@archive_root_dir/appspec.yaml").returns("APP SPEC") + @command_executor.execute_command(@command, deployment_spec) + end + end + + context "when executing the DownloadBundle command" do + setup do + InstanceAgent::LinuxUtil.stubs(:extract_tar) + InstanceAgent::LinuxUtil.stubs(:extract_tgz) + @command.command_name = "DownloadBundle" + @http = mock + @mock_file = mock + Net::HTTP.stubs(:start).yields(@http) + File.stubs(:open).returns @mock_file + Dir.stubs(:entries).returns [] + @mock_file.stubs(:close) + @http.stubs(:request_get) + @s3 = mock + @s3.stubs(:config).returns("hello") + Aws::S3::Client.stubs(:new).returns(@s3) + end + + context "when GitHub revision specified" do + setup do + File.stubs(:directory?).with(@archive_root_dir).returns(true) + FileUtils.stubs(:mv) + FileUtils.stubs(:rmdir) + @mock_file.stubs(:write) + @deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id.to_s, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "Revision" => { + "RevisionType" => "GitHub", + "GitHubRevision" => { + 'Account' => 'account', + 'Repository' => 'repository', + 'CommitId' => 'commitid', + } + } + }) + + ENV['AWS_SSL_CA_DIRECTORY'] = 'aws_ssl_ca_directory' + @mock_uri = mock + uri_options = {:ssl_verify_mode => OpenSSL::SSL::VERIFY_PEER, :redirect => true, :ssl_ca_cert => ENV['AWS_SSL_CA_DIRECTORY']} + @mock_buffer = mock + @mock_github_response = mock + @mock_github_response.stubs(:read).returns(@mock_buffer) + @mock_uri.stubs(:open).with(uri_options).yields(@mock_github_response) + end + + should 'download file from github' do + URI.expects(:parse).with("https://api.github.com/repos/account/repository/tarball/commitid").returns(@mock_uri) + @command_executor.execute_command(@command, @deployment_spec) + end + + context 'when Github bundle_type is specified' do + setup do + @bundle_type = 'zip' + + @deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id.to_s, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "Revision" => { + "RevisionType" => "GitHub", + "GitHubRevision" => { + 'Account' => 'account', + 'Repository' => 'repository', + 'CommitId' => 'commitid', + 'BundleType' => @bundle_type + } + } + }) + end + + should 'downloads from github with the corresponding format' do + URI.expects(:parse).with("https://api.github.com/repos/account/repository/zipball/commitid").returns(@mock_uri) + Zip::File.expects(:open).with(File.join(@deployment_root_dir, 'bundle.tar')) + @command_executor.execute_command(@command, @deployment_spec) + end + end + end + + context "when creating S3 options" do + +<<<<<<< HEAD + should "use right signature version" do +======= + should "use right signature version" do +>>>>>>> 3515520... CDVSA-185 making appspec filename configurable and support yml and yaml extensions + assert_equal 'v4', @command_executor.s3_options[:signature_version] + end + + context "when override endpoint provided" do + setup do + InstanceAgent::Config.config[:s3_endpoint_override] = "https://example.override.endpoint.com" + end + should "use the override endpoint" do + assert_equal "https://example.override.endpoint.com", @command_executor.s3_options[:endpoint].to_s + end + end + + context "when no override endpoint provided and not using fips" do + setup do + InstanceAgent::Config.config[:s3_endpoint_override] = nil + InstanceAgent::Config.config[:use_fips_mode] = false + end + should "use correct region and custom endpoint" do + assert_equal 'us-east-1', @command_executor.s3_options[:region] + assert_false @command_executor.s3_options.include? :endpoint + end + end + + context "when no override endpoint provided and using fips" do + setup do + InstanceAgent::Config.config[:s3_endpoint_override] = nil + InstanceAgent::Config.config[:use_fips_mode] = true + end + should "use correct region and custom endpoint" do + assert_equal 'us-east-1', @command_executor.s3_options[:region] + assert_true @command_executor.s3_options.include? :endpoint + end + end + end + + context "downloading bundle from S3" do + setup do + File.expects(:open).with(File.join(@deployment_root_dir, 'bundle.tar'), 'wb').yields(@mock_file) + @object = mock + @s3.stubs(:get_object).returns(@object) + @io = mock + @object.stubs(:etag).returns("myetag") + @deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id.to_s, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "Revision" => { + "RevisionType" => "S3", + "S3Revision" => { + "Bucket" => "mybucket", + "Key" => "mykey", + "BundleType" => "tar" + } + } + }) + end + + context "when setting up the S3 client" do + setup do + ENV['AWS_REGION'] = nil + InstanceMetadata.stubs(:region).returns('us-east-1') + end + + should "read from the InstanceMetadata to get the region" do + InstanceMetadata.expects(:region) + @command_executor.execute_command(@command, @deployment_spec) + end + end + + should "verify etag" do + @deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id.to_s, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "Revision" => { + "RevisionType" => "S3", + "S3Revision" => { + "Bucket" => "mybucket", + "Key" => "mykey", + "BundleType" => "tar", + "ETag" => "myetag" + } + } + }) + @command_executor.execute_command(@command, @deployment_spec) + end + + should "verify etag that contains quotations still matches" do + @object.stubs(:etag).returns("\"myetag\"") + @deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id.to_s, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "Revision" => { + "RevisionType" => "S3", + "S3Revision" => { + "Bucket" => "mybucket", + "Key" => "mykey", + "BundleType" => "tar", + "ETag" => "myetag" + } + } + }) + @command_executor.execute_command(@command, @deployment_spec) + end + + should "verify version" do + @object.stubs(:body).with(:bucket => "mybucket", :key => "mykey", :version_id => "myversion").returns(@io) + @deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id.to_s, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "Revision" => { + "RevisionType" => "S3", + "S3Revision" => { + "Bucket" => "mybucket", + "Key" => "mykey", + "BundleType" => "tar", + "Version" => "myversion" + } + } + }) + @command_executor.execute_command(@command, @deployment_spec) + end + + should "call zip for zip BundleTypes" do + @deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id.to_s, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "Revision" => { + "RevisionType" => "S3", + "S3Revision" => { + "Bucket" => "mybucket", + "Key" => "mykey", + "BundleType" => "zip" + } + } + }) + Zip::File.expects(:open).with(File.join(@deployment_root_dir, 'bundle.tar')) + @command_executor.execute_command(@command, @deployment_spec) + end + + should "call extract_tgz for Gzipped tar BundleTypes" do + @deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id.to_s, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "Revision" => { + "RevisionType" => "S3", + "S3Revision" => { + "Bucket" => "mybucket", + "Key" => "mykey", + "BundleType" => "tgz" + } + } + }) + InstanceAgent::LinuxUtil.expects(:extract_tgz).with(File.join(@deployment_root_dir, 'bundle.tar'), @archive_root_dir) + @command_executor.execute_command(@command, @deployment_spec) + end + + should "call extract_tar for tar BundleTypes" do + @deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id.to_s, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "Revision" => { + "RevisionType" => "S3", + "S3Revision" => { + "Bucket" => "mybucket", + "Key" => "mykey", + "BundleType" => "tar" + } + } + }) + InstanceAgent::LinuxUtil.expects(:extract_tar).with(File.join(@deployment_root_dir, 'bundle.tar'), @archive_root_dir) + @command_executor.execute_command(@command, @deployment_spec) + end + + end + + context "extract bundle from local file" do + setup do + InstanceAgent::LinuxUtil.stubs(:extract_tgz) + @command.command_name = "DownloadBundle" + @mock_file = mock + @mock_file_location = '/mock/file/location.tgz' + File.stubs(:symlink) + Dir.stubs(:entries).returns [] + @mock_file.stubs(:close) + + @deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id.to_s, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "Revision" => { + "RevisionType" => "Local File", + "LocalRevision" => { + "Location" => @mock_file_location, + "BundleType" => 'tgz' + } + } + }) + end + + should 'symlink the file to the bundle location' do + File.expects(:symlink).with(@mock_file_location, File.join(@deployment_root_dir, 'bundle.tar')) + @command_executor.execute_command(@command, @deployment_spec) + end + end + + context "handle bundle from local directory" do + setup do + @command.command_name = "DownloadBundle" + @mock_directory_location = '/mock/directory/location/' + FileUtils.stubs(:cp_r) + Dir.stubs(:entries).returns [] + @mock_file.stubs(:close) + + @deployment_spec = generate_signed_message_for({ + "DeploymentId" => @deployment_id.to_s, + "DeploymentGroupId" => @deployment_group_id.to_s, + "ApplicationName" => @application_name, + "DeploymentGroupName" => @deployment_group_name, + "Revision" => { + "RevisionType" => "Local Directory", + "LocalRevision" => { + "Location" => @mock_directory_location, + "BundleType" => 'directory' + } + } + }) + end + + should 'copy recursively the directory to the bundle location' do + FileUtils.expects(:cp_r).with(@mock_directory_location, @archive_root_dir) + @command_executor.execute_command(@command, @deployment_spec) + end + end + + should "unpack the bundle to the right directory" do + InstanceAgent::LinuxUtil.expects(:extract_tar).with(File.join(@deployment_root_dir, 'bundle.tar'), @archive_root_dir) + @command_executor.execute_command(@command, @deployment_spec) + end + + should "remove the directory before unpacking" do + call_sequence = sequence("call sequence") + FileUtils.expects(:rm_rf).with(@archive_root_dir).in_sequence(call_sequence) + InstanceAgent::LinuxUtil.expects(:extract_tar).in_sequence(call_sequence) + @command_executor.execute_command(@command, @deployment_spec) + end + + should "idempotently create the instructions directory" do + FileUtils.expects(:mkdir_p).with(@deployment_instructions_dir) + + @command_executor.execute_command(@command, @deployment_spec) + end + + should "write the archive root dir to the install instructions file" do + mock_file = mock + File.expects(:open).with(@most_recent_install_file_location, 'w+').yields(mock_file) + mock_file.expects(:write).with(@deployment_root_dir) + + @command_executor.execute_command(@command, @deployment_spec) + end + end + + context "I have an empty app spec (for script mapping)" do + setup do + File.stubs(:read).with(File.join(@archive_root_dir, 'appspec.yml')).returns(nil) + InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::ApplicationSpecification.stubs(:parse).returns(nil) + @hook_executor_constructor_hash = { + :application_name => @application_name, + :deployment_id => @deployment_id, + :deployment_group_name => @deployment_group_name, + :deployment_group_id => @deployment_group_id, + :deployment_creator => @deployment_creator, + :deployment_type => @deployment_type, + :deployment_root_dir => @deployment_root_dir, + :last_successful_deployment_dir => nil, + :most_recent_deployment_dir => nil, + :app_spec_path => 'appspec.yml'} + @mock_hook_executor = mock + end + + context "BeforeBlockTraffic" do + setup do + @command.command_name = "BeforeBlockTraffic" + @hook_executor_constructor_hash[:lifecycle_event] = "BeforeBlockTraffic" + end + + should "call execute a hook executor object with BeforeBlockTraffic as one of the params" do + HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) + @mock_hook_executor.expects(:execute) + @command_executor.execute_command(@command, @deployment_spec) + end + end + + context "AfterBlockTraffic" do + setup do + @command.command_name = "AfterBlockTraffic" + @hook_executor_constructor_hash[:lifecycle_event] = "AfterBlockTraffic" + end + + should "call execute a hook executor object with AfterBlockTraffic as one of the params" do + HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) + @mock_hook_executor.expects(:execute) + @command_executor.execute_command(@command, @deployment_spec) + end + end + + context "ApplicationStop" do + setup do + @command.command_name = "ApplicationStop" + @hook_executor_constructor_hash[:lifecycle_event] = "ApplicationStop" + end + + should "call execute a hook executor object with ApplicationStop as one of the params" do + HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) + @mock_hook_executor.expects(:execute) + @command_executor.execute_command(@command, @deployment_spec) + end + end + + context "BeforeInstall" do + setup do + @command.command_name = "BeforeInstall" + @hook_executor_constructor_hash[:lifecycle_event] = "BeforeInstall" + end + + should "call execute a hook executor object with BeforeInstall as one of the params" do + HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) + @mock_hook_executor.expects(:execute) + @command_executor.execute_command(@command, @deployment_spec) + end + end + + context "AfterInstall" do + setup do + @command.command_name = "AfterInstall" + @hook_executor_constructor_hash[:lifecycle_event] = "AfterInstall" + end + + should "call execute a hook executor object with AfterInstall as one of the params" do + HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) + @mock_hook_executor.expects(:execute) + @command_executor.execute_command(@command, @deployment_spec) + end + end + + context "ApplicationStart" do + setup do + @command.command_name = "ApplicationStart" + @hook_executor_constructor_hash[:lifecycle_event] = "ApplicationStart" + end + + should "call execute a hook executor object with ApplicationStart as one of the params" do + HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) + @mock_hook_executor.expects(:execute) + @command_executor.execute_command(@command, @deployment_spec) + end + end + + context "BeforeAllowTraffic" do + setup do + @command.command_name = "BeforeAllowTraffic" + @hook_executor_constructor_hash[:lifecycle_event] = "BeforeAllowTraffic" + end + + should "call execute a hook executor object with BeforeAllowTraffic as one of the params" do + HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) + @mock_hook_executor.expects(:execute) + @command_executor.execute_command(@command, @deployment_spec) + end + end + + context "AfterAllowTraffic" do + setup do + @command.command_name = "AfterAllowTraffic" + @hook_executor_constructor_hash[:lifecycle_event] = "AfterAllowTraffic" + end + + should "call execute a hook executor object with AfterAllowTraffic as one of the params" do + HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) + @mock_hook_executor.expects(:execute) + @command_executor.execute_command(@command, @deployment_spec) + end + end + + context "ValidateService" do + setup do + @command.command_name = "ValidateService" + @hook_executor_constructor_hash[:lifecycle_event] = "ValidateService" + end + + should "call execute a hook executor object with ValidateService as one of the params" do + HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) + @mock_hook_executor.expects(:execute) + @command_executor.execute_command(@command, @deployment_spec) + end + end + end + + #non 1:1 mapping tests + context "one command hooks to multiple lifecycle events" do + setup do + @command.command_name = "TestCommand" + @test_hook_mapping = { "TestCommand" => ["lifecycle_event_1","lifecycle_event_2"]} + @deploy_control_client = mock + @command_executor = InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor.new({ + :deploy_control_client => @deploy_control_client, + :hook_mapping => @test_hook_mapping}) + hook_executor_constructor_hash = { + :deployment_root_dir => @deployment_root_dir, + :application_name => @application_name, + :deployment_id => @deployment_id, + :deployment_group_name => @deployment_group_name, + :deployment_group_id => @deployment_group_id, + :deployment_creator => @deployment_creator, + :deployment_type => @deployment_type, + :last_successful_deployment_dir => nil, + :most_recent_deployment_dir => nil, + :app_spec_path => 'appspec.yml'} + @hook_executor_constructor_hash_1 = hook_executor_constructor_hash.merge({:lifecycle_event => "lifecycle_event_1"}) + @hook_executor_constructor_hash_2 = hook_executor_constructor_hash.merge({:lifecycle_event => "lifecycle_event_2"}) + @mock_hook_executor = mock + end + + should "call both lifecycle events" do + HookExecutor.expects(:new).with(@hook_executor_constructor_hash_1).returns(@mock_hook_executor) + HookExecutor.expects(:new).with(@hook_executor_constructor_hash_2).returns(@mock_hook_executor) + @mock_hook_executor.expects(:execute).twice + + @command_executor.execute_command(@command, @deployment_spec) + end + + context "when the first script is forced to fail" do + setup do + HookExecutor.stubs(:new).with(@hook_executor_constructor_hash_1).raises("failed to create hook command") + end + + should "calls lifecycle event 1 and fails but not lifecycle event 2" do + assert_raised_with_message('failed to create hook command') do + @command_executor.execute_command(@command, @deployment_spec) + end + HookExecutor.expects(:new).with(@hook_executor_constructor_hash_2).never + end + end + end + end + end +end From ec2f1c0a6eaa8edcf3444b8b3c59173fc42d1965 Mon Sep 17 00:00:00 2001 From: Qionglin Dai Date: Tue, 23 Feb 2021 17:57:07 +0000 Subject: [PATCH 4/7] remove reletive path from Linux extract_zip method --- lib/instance_agent/platform/linux_util.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/instance_agent/platform/linux_util.rb b/lib/instance_agent/platform/linux_util.rb index 6708f39f..08b1c933 100644 --- a/lib/instance_agent/platform/linux_util.rb +++ b/lib/instance_agent/platform/linux_util.rb @@ -51,11 +51,8 @@ def self.extract_tar(bundle_file, dst) def self.extract_zip(bundle_file, dst) log(:debug, "extract_zip - dst : #{dst}") FileUtils.mkdir_p(dst) - working_dir = FileUtils.pwd() absolute_bundle_path = File.expand_path(bundle_file) - FileUtils.cd(dst) - execute_zip_command("unzip -qo #{absolute_bundle_path}") - FileUtils.cd(working_dir) + execute_zip_command("unzip -qo #{absolute_bundle_path} -d #{dst}") end def self.extract_tgz(bundle_file, dst) From 30f233f733311c969d1769d90d2e8000c2a5d1ac Mon Sep 17 00:00:00 2001 From: Kaiwen Sun Date: Thu, 25 Feb 2021 15:07:23 -0800 Subject: [PATCH 5/7] Bump up agent version to 1.3.2 --- codedeploy_agent.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codedeploy_agent.gemspec b/codedeploy_agent.gemspec index 7d178dad..c0d7b67b 100644 --- a/codedeploy_agent.gemspec +++ b/codedeploy_agent.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |spec| spec.name = 'aws_codedeploy_agent' - spec.version = '1.3.1' + spec.version = '1.3.2' spec.summary = 'Packages AWS CodeDeploy agent libraries' spec.description = 'AWS CodeDeploy agent is responsible for doing the actual work of deploying software on an individual EC2 instance' spec.author = 'Amazon Web Services' From 09f92e7a1b017fdbd764e088a2d1fc3fd02e220c Mon Sep 17 00:00:00 2001 From: Mike Jones Date: Fri, 12 Mar 2021 21:30:41 +0000 Subject: [PATCH 6/7] Fix ubuntu 20.04 install, update bug Prior to this change, running the install and update scripts on an Ubuntu 20.04 instance failed unless the output was directed to a file. This change makes them work without that workaround. --- bin/install | 11 +++++++++++ bin/update | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/bin/install b/bin/install index 83f9913a..43a21270 100755 --- a/bin/install +++ b/bin/install @@ -14,6 +14,17 @@ class Proxy @targets = targets end + def path + @targets.map do |target| + if target.respond_to?(:path) + target.__send__(:path) + else + # default to to_s since it's just used as a label for log statements. + target.__send__(:to_s) + end + end + end + protected def method_missing(name, *args, &block) diff --git a/bin/update b/bin/update index 7bc0ef13..3d9cc6aa 100755 --- a/bin/update +++ b/bin/update @@ -14,6 +14,17 @@ class Proxy @targets = targets end + def path + @targets.map do |target| + if target.respond_to?(:path) + target.__send__(:path) + else + # default to to_s since it's just used as a label for log statements. + target.__send__(:to_s) + end + end + end + protected def method_missing(name, *args, &block) From cd755a8a0697097616ef74bf40f57419914ecb6c Mon Sep 17 00:00:00 2001 From: Phil Strong Date: Fri, 21 May 2021 12:57:47 +0000 Subject: [PATCH 7/7] fix bad merge conflict --- .../codedeploy/command_executor_test.rb | 5 - .../codedeploy/command_executor_test.rb.orig | 873 ------------------ 2 files changed, 878 deletions(-) delete mode 100644 test/instance_agent/plugins/codedeploy/command_executor_test.rb.orig diff --git a/test/instance_agent/plugins/codedeploy/command_executor_test.rb b/test/instance_agent/plugins/codedeploy/command_executor_test.rb index 9a44e2ed..c6f8f5e7 100644 --- a/test/instance_agent/plugins/codedeploy/command_executor_test.rb +++ b/test/instance_agent/plugins/codedeploy/command_executor_test.rb @@ -405,12 +405,7 @@ def generate_signed_message_for(map) end context "when creating S3 options" do - -<<<<<<< HEAD - should "use right signature version" do -======= should "use right signature version" do ->>>>>>> 3515520... CDVSA-185 making appspec filename configurable and support yml and yaml extensions assert_equal 'v4', @command_executor.s3_options[:signature_version] end diff --git a/test/instance_agent/plugins/codedeploy/command_executor_test.rb.orig b/test/instance_agent/plugins/codedeploy/command_executor_test.rb.orig deleted file mode 100644 index 9a44e2ed..00000000 --- a/test/instance_agent/plugins/codedeploy/command_executor_test.rb.orig +++ /dev/null @@ -1,873 +0,0 @@ -require 'test_helper' -require 'certificate_helper' -require 'stringio' -require 'aws-sdk-s3' - -require 'aws/codedeploy/local/deployer' - -class CodeDeployPluginCommandExecutorTest < InstanceAgentTestCase - - include InstanceAgent::Plugins::CodeDeployPlugin - def generate_signed_message_for(map) - message = @cert_helper.sign_message(map.to_json) - spec = OpenStruct.new({ :payload => message }) - spec.format = "PKCS7/JSON" - - return spec - end - - context 'The CodeDeploy Plugin Command Executor' do - setup do - @test_hook_mapping = { "BeforeBlockTraffic"=>["BeforeBlockTraffic"], - "AfterBlockTraffic"=>["AfterBlockTraffic"], - "ApplicationStop"=>["ApplicationStop"], - "BeforeInstall"=>["BeforeInstall"], - "AfterInstall"=>["AfterInstall"], - "ApplicationStart"=>["ApplicationStart"], - "BeforeAllowTraffic"=>["BeforeAllowTraffic"], - "AfterAllowTraffic"=>["AfterAllowTraffic"], - "ValidateService"=>["ValidateService"]} - @deploy_control_client = mock - @command_executor = InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor.new({ - :deploy_control_client => @deploy_control_client, - :hook_mapping => @test_hook_mapping}) - @aws_region = 'us-east-1' - @partition = 'aws' - @domain = 'amazonaws.com' - InstanceMetadata.stubs(:region).returns(@aws_region) - InstanceMetadata.stubs(:partition).returns(@partition) - InstanceMetadata.stubs(:domain).returns(@domain) - end - - context "deployment_system method" do - should "always return CodeDeploy" do - assert_equal "CodeDeploy", @command_executor.deployment_system - end - end - - context "when executing a command" do - - setup do - @cert_helper = CertificateHelper.new - @deployment_id = SecureRandom.uuid - @deployment_group_name = "TestDeploymentGroup" - @application_name = "TestApplicationName" - @deployment_group_id = "foo" - @deployment_creator = "User" - @deployment_type = "IN_PLACE" - @s3Revision = { - "Bucket" => "mybucket", - "Key" => "mykey", - "BundleType" => "tar" - } - @file_exists_behavior = "RETAIN" - @agent_actions_overrides_map = {"FileExistsBehavior" => @file_exists_behavior} - @agent_actions_overrides = {"AgentOverrides" => @agent_actions_overrides_map} - @deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "DeploymentCreator" => @deployment_creator, - "DeploymentType" => @deployment_type, - "AgentActionOverrides" => @agent_actions_overrides, - "Revision" => { - "RevisionType" => "S3", - "S3Revision" => @s3Revision - } - }) - @command = Aws::CodeDeployCommand::Types::HostCommandInstance.new( - :host_command_identifier => "command-1", - :deployment_execution_id => "test-execution") - @root_dir = '/tmp/codedeploy/' - @deployment_root_dir = File.join(@root_dir, @deployment_group_id.to_s, @deployment_id.to_s) - @deployment_instructions_dir = File.join(@root_dir, 'deployment-instructions') - @archive_root_dir = File.join(@deployment_root_dir, 'deployment-archive') - ProcessManager::Config.config[:root_dir] = @root_dir - - FileUtils.stubs(:mkdir_p) - File.stubs(:directory?).with(@deployment_root_dir).returns(true) - @last_successful_install_file_location = File.join(@deployment_instructions_dir, "#{@deployment_group_id}_last_successful_install") - @most_recent_install_file_location = File.join(@deployment_instructions_dir, "#{@deployment_group_id}_most_recent_install") - end - - context "when executing an unknown command" do - setup do - @command.command_name = "unknown-command" - end - - should "not create the deployment root directory" do - # Need to unstub the :mkdir_p method otherwise the never expectation doesn't work - FileUtils.unstub(:mkdir_p) - FileUtils.expects(:mkdir_p).never - - assert_raised_with_message('Unsupported command type: unknown-command.', InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor::InvalidCommandNameFailure) do - @command_executor.execute_command(@command, @deployment_spec) - end - end - - should "throw an exception" do - assert_raised_with_message('Unsupported command type: unknown-command.', InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor::InvalidCommandNameFailure) do - @command_executor.execute_command(@command, @deployment_spec) - end - end - end - - context "when executing a valid command" do - setup do - @command.command_name = "Install" - @command_executor.stubs(:install) - end - - should "create the deployment root directory" do - FileUtils.expects(:mkdir_p).with(@deployment_root_dir) - - @command_executor.execute_command(@command, @deployment_spec) - end - - context "when failed to create root directory" do - setup do - File.stubs(:directory?).with(@deployment_root_dir).returns(false) - end - - should "raise an exception" do - assert_raised_with_message("Error creating deployment root directory #{@deployment_root_dir}") do - @command_executor.execute_command(@command, @deployment_spec) - end - end - end - end - - context "when executing the Install command" do - - setup do - @command.command_name = "Install" - InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::ApplicationSpecification.stubs(:parse).returns(@app_spec) - @installer = stub("installer", :install => nil) - Installer.stubs(:new).returns(@installer) - File.stubs(:directory?).with(@deployment_instructions_dir).returns(true) - File.stubs(:exist?).with(@last_successful_install_file_location).returns(true) - File.stubs(:exist?).with(@archive_root_dir).returns(true) - File.stubs(:open).with(@last_successful_install_file_location, 'w+') - File.stubs(:open).with(@last_successful_install_file_location) - - @app_spec = mock("parsed application specification") - File.stubs(:exist?).with("#@archive_root_dir/appspec.yml").returns(true) - File.stubs(:read).with("#@archive_root_dir/appspec.yml").returns("APP SPEC") - ApplicationSpecification::ApplicationSpecification.stubs(:parse).with("APP SPEC").returns(@app_spec) - end - - should "create an appropriate Installer" do - Installer. - expects(:new). - with(:deployment_instructions_dir => @deployment_instructions_dir, - :deployment_archive_dir => @archive_root_dir, - :file_exists_behavior => @file_exists_behavior). - returns(@installer) - - @command_executor.execute_command(@command, @deployment_spec) - end - - should "perform the installation for the current IG, revision and app spec" do - @installer.expects(:install).with(@deployment_group_id, @app_spec) - - @command_executor.execute_command(@command, @deployment_spec) - end - - should "write the archive root dir to the install instructions file" do - mock_file = mock - File.expects(:open).with(@last_successful_install_file_location, 'w+').yields(mock_file) - mock_file.expects(:write).with(@deployment_root_dir) - - @command_executor.execute_command(@command, @deployment_spec) - end - - should 'raise ArgumentError if appspec contains unknown hook and deployment_spec includes all_possible_lifecycle_events' do - all_possible_lifecycle_events = ['ExampleLifecycleEvent', 'SecondLifecycleEvent'] - deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "DeploymentCreator" => @deployment_creator, - "DeploymentType" => @deployment_type, - "AgentActionOverrides" => @agent_actions_overrides, - "AllPossibleLifecycleEvents" => all_possible_lifecycle_events, - "Revision" => { - "RevisionType" => "S3", - "S3Revision" => @s3Revision - } - }) - - app_spec = mock("parsed application specification") - app_spec_hooks = {'UnknownHook' => nil} - app_spec.expects(:hooks).returns(app_spec_hooks) - File.stubs(:read).with("#@archive_root_dir/appspec.yml").returns("APP SPEC") - ApplicationSpecification::ApplicationSpecification.stubs(:parse).with("APP SPEC").returns(app_spec) - unknown_hooks = app_spec_hooks.merge(@test_hook_mapping) - assert_raised_with_message("appspec.yml file contains unknown lifecycle events: #{unknown_hooks.keys}", ArgumentError) do - @command_executor.execute_command(@command, deployment_spec) - end - end - - should 'raise ArgumentError if appspec custom hook specified that does not exist in appspec' do - all_possible_lifecycle_events = AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS + ['ExampleLifecycleEvent', 'SecondLifecycleEvent', 'CustomHookNotInAppspec'] - deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "DeploymentCreator" => @deployment_creator, - "DeploymentType" => @deployment_type, - "AgentActionOverrides" => @agent_actions_overrides, - "AllPossibleLifecycleEvents" => all_possible_lifecycle_events, - "Revision" => { - "RevisionType" => "S3", - "S3Revision" => @s3Revision - } - }) - - app_spec = mock("parsed application specification") - app_spec_hooks = InstanceAgent::Plugins::CodeDeployPlugin::CommandPoller::DEFAULT_HOOK_MAPPING.merge({'ExampleLifecycleEvent' => nil, 'SecondLifecycleEvent' => nil}) - app_spec.expects(:hooks).twice.returns(app_spec_hooks) - File.stubs(:read).with("#@archive_root_dir/appspec.yml").returns("APP SPEC") - ApplicationSpecification::ApplicationSpecification.stubs(:parse).with("APP SPEC").returns(app_spec) - assert_raised_with_message("You specified a lifecycle event which is not a default one and doesn't exist in your appspec.yml file: CustomHookNotInAppspec", ArgumentError) do - @command_executor.execute_command(@command, deployment_spec) - end - end - - should 'honor the AppSpecFilename command variable' do - deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "DeploymentCreator" => @deployment_creator, - "DeploymentType" => @deployment_type, - "AgentActionOverrides" => @agent_actions_overrides, - "AppSpecFilename" => "appspec-override.yaml", - "Revision" => { - "RevisionType" => "S3", - "S3Revision" => @s3Revision - } - }) - - File.expects(:exist?).with("#@archive_root_dir/appspec-override.yaml").returns(true) - File.expects(:read).with("#@archive_root_dir/appspec-override.yaml").returns("APP SPEC") - @command_executor.execute_command(@command, deployment_spec) - end - - should 'fallback to appspec.yaml if provided AppSpecFilename variable points to a file which does\'t exist' do - deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "DeploymentCreator" => @deployment_creator, - "DeploymentType" => @deployment_type, - "AgentActionOverrides" => @agent_actions_overrides, - "AppSpecFilename" => "appspec-override.yaml", - "Revision" => { - "RevisionType" => "S3", - "S3Revision" => @s3Revision - } - }) - - File.expects(:exist?).with("#@archive_root_dir/appspec-override.yaml").returns(false) - File.expects(:exist?).with("#@archive_root_dir/appspec.yaml").returns(true) - File.expects(:read).with("#@archive_root_dir/appspec.yaml").returns("APP SPEC") - @command_executor.execute_command(@command, deployment_spec) - end - should 'fallback to appspec.yaml if both AppSpecFilename variable and appspec.yaml don\'t exist' do - deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "DeploymentCreator" => @deployment_creator, - "DeploymentType" => @deployment_type, - "AgentActionOverrides" => @agent_actions_overrides, - "AppSpecFilename" => "appspec-override.yaml", - "Revision" => { - "RevisionType" => "S3", - "S3Revision" => @s3Revision - } - }) - - File.expects(:exist?).with("#@archive_root_dir/appspec-override.yaml").returns(false) - File.expects(:exist?).with("#@archive_root_dir/appspec.yaml").returns(false) - File.expects(:read).with("#@archive_root_dir/appspec.yml").returns("APP SPEC") - @command_executor.execute_command(@command, deployment_spec) - end - - should 'fallback to appspec.yaml if appspec.yml is not there and no AppSpecFilename arg is specified' do - deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "DeploymentCreator" => @deployment_creator, - "DeploymentType" => @deployment_type, - "AgentActionOverrides" => @agent_actions_overrides, - "Revision" => { - "RevisionType" => "S3", - "S3Revision" => @s3Revision - } - }) - - File.expects(:exist?).with("#@archive_root_dir/appspec.yml").returns(false) - File.expects(:exist?).with("#@archive_root_dir/appspec.yaml").returns(true) - File.expects(:read).with("#@archive_root_dir/appspec.yaml").returns("APP SPEC") - @command_executor.execute_command(@command, deployment_spec) - end - end - - context "when executing the DownloadBundle command" do - setup do - InstanceAgent::LinuxUtil.stubs(:extract_tar) - InstanceAgent::LinuxUtil.stubs(:extract_tgz) - @command.command_name = "DownloadBundle" - @http = mock - @mock_file = mock - Net::HTTP.stubs(:start).yields(@http) - File.stubs(:open).returns @mock_file - Dir.stubs(:entries).returns [] - @mock_file.stubs(:close) - @http.stubs(:request_get) - @s3 = mock - @s3.stubs(:config).returns("hello") - Aws::S3::Client.stubs(:new).returns(@s3) - end - - context "when GitHub revision specified" do - setup do - File.stubs(:directory?).with(@archive_root_dir).returns(true) - FileUtils.stubs(:mv) - FileUtils.stubs(:rmdir) - @mock_file.stubs(:write) - @deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id.to_s, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "Revision" => { - "RevisionType" => "GitHub", - "GitHubRevision" => { - 'Account' => 'account', - 'Repository' => 'repository', - 'CommitId' => 'commitid', - } - } - }) - - ENV['AWS_SSL_CA_DIRECTORY'] = 'aws_ssl_ca_directory' - @mock_uri = mock - uri_options = {:ssl_verify_mode => OpenSSL::SSL::VERIFY_PEER, :redirect => true, :ssl_ca_cert => ENV['AWS_SSL_CA_DIRECTORY']} - @mock_buffer = mock - @mock_github_response = mock - @mock_github_response.stubs(:read).returns(@mock_buffer) - @mock_uri.stubs(:open).with(uri_options).yields(@mock_github_response) - end - - should 'download file from github' do - URI.expects(:parse).with("https://api.github.com/repos/account/repository/tarball/commitid").returns(@mock_uri) - @command_executor.execute_command(@command, @deployment_spec) - end - - context 'when Github bundle_type is specified' do - setup do - @bundle_type = 'zip' - - @deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id.to_s, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "Revision" => { - "RevisionType" => "GitHub", - "GitHubRevision" => { - 'Account' => 'account', - 'Repository' => 'repository', - 'CommitId' => 'commitid', - 'BundleType' => @bundle_type - } - } - }) - end - - should 'downloads from github with the corresponding format' do - URI.expects(:parse).with("https://api.github.com/repos/account/repository/zipball/commitid").returns(@mock_uri) - Zip::File.expects(:open).with(File.join(@deployment_root_dir, 'bundle.tar')) - @command_executor.execute_command(@command, @deployment_spec) - end - end - end - - context "when creating S3 options" do - -<<<<<<< HEAD - should "use right signature version" do -======= - should "use right signature version" do ->>>>>>> 3515520... CDVSA-185 making appspec filename configurable and support yml and yaml extensions - assert_equal 'v4', @command_executor.s3_options[:signature_version] - end - - context "when override endpoint provided" do - setup do - InstanceAgent::Config.config[:s3_endpoint_override] = "https://example.override.endpoint.com" - end - should "use the override endpoint" do - assert_equal "https://example.override.endpoint.com", @command_executor.s3_options[:endpoint].to_s - end - end - - context "when no override endpoint provided and not using fips" do - setup do - InstanceAgent::Config.config[:s3_endpoint_override] = nil - InstanceAgent::Config.config[:use_fips_mode] = false - end - should "use correct region and custom endpoint" do - assert_equal 'us-east-1', @command_executor.s3_options[:region] - assert_false @command_executor.s3_options.include? :endpoint - end - end - - context "when no override endpoint provided and using fips" do - setup do - InstanceAgent::Config.config[:s3_endpoint_override] = nil - InstanceAgent::Config.config[:use_fips_mode] = true - end - should "use correct region and custom endpoint" do - assert_equal 'us-east-1', @command_executor.s3_options[:region] - assert_true @command_executor.s3_options.include? :endpoint - end - end - end - - context "downloading bundle from S3" do - setup do - File.expects(:open).with(File.join(@deployment_root_dir, 'bundle.tar'), 'wb').yields(@mock_file) - @object = mock - @s3.stubs(:get_object).returns(@object) - @io = mock - @object.stubs(:etag).returns("myetag") - @deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id.to_s, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "Revision" => { - "RevisionType" => "S3", - "S3Revision" => { - "Bucket" => "mybucket", - "Key" => "mykey", - "BundleType" => "tar" - } - } - }) - end - - context "when setting up the S3 client" do - setup do - ENV['AWS_REGION'] = nil - InstanceMetadata.stubs(:region).returns('us-east-1') - end - - should "read from the InstanceMetadata to get the region" do - InstanceMetadata.expects(:region) - @command_executor.execute_command(@command, @deployment_spec) - end - end - - should "verify etag" do - @deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id.to_s, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "Revision" => { - "RevisionType" => "S3", - "S3Revision" => { - "Bucket" => "mybucket", - "Key" => "mykey", - "BundleType" => "tar", - "ETag" => "myetag" - } - } - }) - @command_executor.execute_command(@command, @deployment_spec) - end - - should "verify etag that contains quotations still matches" do - @object.stubs(:etag).returns("\"myetag\"") - @deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id.to_s, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "Revision" => { - "RevisionType" => "S3", - "S3Revision" => { - "Bucket" => "mybucket", - "Key" => "mykey", - "BundleType" => "tar", - "ETag" => "myetag" - } - } - }) - @command_executor.execute_command(@command, @deployment_spec) - end - - should "verify version" do - @object.stubs(:body).with(:bucket => "mybucket", :key => "mykey", :version_id => "myversion").returns(@io) - @deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id.to_s, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "Revision" => { - "RevisionType" => "S3", - "S3Revision" => { - "Bucket" => "mybucket", - "Key" => "mykey", - "BundleType" => "tar", - "Version" => "myversion" - } - } - }) - @command_executor.execute_command(@command, @deployment_spec) - end - - should "call zip for zip BundleTypes" do - @deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id.to_s, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "Revision" => { - "RevisionType" => "S3", - "S3Revision" => { - "Bucket" => "mybucket", - "Key" => "mykey", - "BundleType" => "zip" - } - } - }) - Zip::File.expects(:open).with(File.join(@deployment_root_dir, 'bundle.tar')) - @command_executor.execute_command(@command, @deployment_spec) - end - - should "call extract_tgz for Gzipped tar BundleTypes" do - @deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id.to_s, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "Revision" => { - "RevisionType" => "S3", - "S3Revision" => { - "Bucket" => "mybucket", - "Key" => "mykey", - "BundleType" => "tgz" - } - } - }) - InstanceAgent::LinuxUtil.expects(:extract_tgz).with(File.join(@deployment_root_dir, 'bundle.tar'), @archive_root_dir) - @command_executor.execute_command(@command, @deployment_spec) - end - - should "call extract_tar for tar BundleTypes" do - @deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id.to_s, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "Revision" => { - "RevisionType" => "S3", - "S3Revision" => { - "Bucket" => "mybucket", - "Key" => "mykey", - "BundleType" => "tar" - } - } - }) - InstanceAgent::LinuxUtil.expects(:extract_tar).with(File.join(@deployment_root_dir, 'bundle.tar'), @archive_root_dir) - @command_executor.execute_command(@command, @deployment_spec) - end - - end - - context "extract bundle from local file" do - setup do - InstanceAgent::LinuxUtil.stubs(:extract_tgz) - @command.command_name = "DownloadBundle" - @mock_file = mock - @mock_file_location = '/mock/file/location.tgz' - File.stubs(:symlink) - Dir.stubs(:entries).returns [] - @mock_file.stubs(:close) - - @deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id.to_s, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "Revision" => { - "RevisionType" => "Local File", - "LocalRevision" => { - "Location" => @mock_file_location, - "BundleType" => 'tgz' - } - } - }) - end - - should 'symlink the file to the bundle location' do - File.expects(:symlink).with(@mock_file_location, File.join(@deployment_root_dir, 'bundle.tar')) - @command_executor.execute_command(@command, @deployment_spec) - end - end - - context "handle bundle from local directory" do - setup do - @command.command_name = "DownloadBundle" - @mock_directory_location = '/mock/directory/location/' - FileUtils.stubs(:cp_r) - Dir.stubs(:entries).returns [] - @mock_file.stubs(:close) - - @deployment_spec = generate_signed_message_for({ - "DeploymentId" => @deployment_id.to_s, - "DeploymentGroupId" => @deployment_group_id.to_s, - "ApplicationName" => @application_name, - "DeploymentGroupName" => @deployment_group_name, - "Revision" => { - "RevisionType" => "Local Directory", - "LocalRevision" => { - "Location" => @mock_directory_location, - "BundleType" => 'directory' - } - } - }) - end - - should 'copy recursively the directory to the bundle location' do - FileUtils.expects(:cp_r).with(@mock_directory_location, @archive_root_dir) - @command_executor.execute_command(@command, @deployment_spec) - end - end - - should "unpack the bundle to the right directory" do - InstanceAgent::LinuxUtil.expects(:extract_tar).with(File.join(@deployment_root_dir, 'bundle.tar'), @archive_root_dir) - @command_executor.execute_command(@command, @deployment_spec) - end - - should "remove the directory before unpacking" do - call_sequence = sequence("call sequence") - FileUtils.expects(:rm_rf).with(@archive_root_dir).in_sequence(call_sequence) - InstanceAgent::LinuxUtil.expects(:extract_tar).in_sequence(call_sequence) - @command_executor.execute_command(@command, @deployment_spec) - end - - should "idempotently create the instructions directory" do - FileUtils.expects(:mkdir_p).with(@deployment_instructions_dir) - - @command_executor.execute_command(@command, @deployment_spec) - end - - should "write the archive root dir to the install instructions file" do - mock_file = mock - File.expects(:open).with(@most_recent_install_file_location, 'w+').yields(mock_file) - mock_file.expects(:write).with(@deployment_root_dir) - - @command_executor.execute_command(@command, @deployment_spec) - end - end - - context "I have an empty app spec (for script mapping)" do - setup do - File.stubs(:read).with(File.join(@archive_root_dir, 'appspec.yml')).returns(nil) - InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::ApplicationSpecification.stubs(:parse).returns(nil) - @hook_executor_constructor_hash = { - :application_name => @application_name, - :deployment_id => @deployment_id, - :deployment_group_name => @deployment_group_name, - :deployment_group_id => @deployment_group_id, - :deployment_creator => @deployment_creator, - :deployment_type => @deployment_type, - :deployment_root_dir => @deployment_root_dir, - :last_successful_deployment_dir => nil, - :most_recent_deployment_dir => nil, - :app_spec_path => 'appspec.yml'} - @mock_hook_executor = mock - end - - context "BeforeBlockTraffic" do - setup do - @command.command_name = "BeforeBlockTraffic" - @hook_executor_constructor_hash[:lifecycle_event] = "BeforeBlockTraffic" - end - - should "call execute a hook executor object with BeforeBlockTraffic as one of the params" do - HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) - @mock_hook_executor.expects(:execute) - @command_executor.execute_command(@command, @deployment_spec) - end - end - - context "AfterBlockTraffic" do - setup do - @command.command_name = "AfterBlockTraffic" - @hook_executor_constructor_hash[:lifecycle_event] = "AfterBlockTraffic" - end - - should "call execute a hook executor object with AfterBlockTraffic as one of the params" do - HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) - @mock_hook_executor.expects(:execute) - @command_executor.execute_command(@command, @deployment_spec) - end - end - - context "ApplicationStop" do - setup do - @command.command_name = "ApplicationStop" - @hook_executor_constructor_hash[:lifecycle_event] = "ApplicationStop" - end - - should "call execute a hook executor object with ApplicationStop as one of the params" do - HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) - @mock_hook_executor.expects(:execute) - @command_executor.execute_command(@command, @deployment_spec) - end - end - - context "BeforeInstall" do - setup do - @command.command_name = "BeforeInstall" - @hook_executor_constructor_hash[:lifecycle_event] = "BeforeInstall" - end - - should "call execute a hook executor object with BeforeInstall as one of the params" do - HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) - @mock_hook_executor.expects(:execute) - @command_executor.execute_command(@command, @deployment_spec) - end - end - - context "AfterInstall" do - setup do - @command.command_name = "AfterInstall" - @hook_executor_constructor_hash[:lifecycle_event] = "AfterInstall" - end - - should "call execute a hook executor object with AfterInstall as one of the params" do - HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) - @mock_hook_executor.expects(:execute) - @command_executor.execute_command(@command, @deployment_spec) - end - end - - context "ApplicationStart" do - setup do - @command.command_name = "ApplicationStart" - @hook_executor_constructor_hash[:lifecycle_event] = "ApplicationStart" - end - - should "call execute a hook executor object with ApplicationStart as one of the params" do - HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) - @mock_hook_executor.expects(:execute) - @command_executor.execute_command(@command, @deployment_spec) - end - end - - context "BeforeAllowTraffic" do - setup do - @command.command_name = "BeforeAllowTraffic" - @hook_executor_constructor_hash[:lifecycle_event] = "BeforeAllowTraffic" - end - - should "call execute a hook executor object with BeforeAllowTraffic as one of the params" do - HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) - @mock_hook_executor.expects(:execute) - @command_executor.execute_command(@command, @deployment_spec) - end - end - - context "AfterAllowTraffic" do - setup do - @command.command_name = "AfterAllowTraffic" - @hook_executor_constructor_hash[:lifecycle_event] = "AfterAllowTraffic" - end - - should "call execute a hook executor object with AfterAllowTraffic as one of the params" do - HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) - @mock_hook_executor.expects(:execute) - @command_executor.execute_command(@command, @deployment_spec) - end - end - - context "ValidateService" do - setup do - @command.command_name = "ValidateService" - @hook_executor_constructor_hash[:lifecycle_event] = "ValidateService" - end - - should "call execute a hook executor object with ValidateService as one of the params" do - HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor) - @mock_hook_executor.expects(:execute) - @command_executor.execute_command(@command, @deployment_spec) - end - end - end - - #non 1:1 mapping tests - context "one command hooks to multiple lifecycle events" do - setup do - @command.command_name = "TestCommand" - @test_hook_mapping = { "TestCommand" => ["lifecycle_event_1","lifecycle_event_2"]} - @deploy_control_client = mock - @command_executor = InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor.new({ - :deploy_control_client => @deploy_control_client, - :hook_mapping => @test_hook_mapping}) - hook_executor_constructor_hash = { - :deployment_root_dir => @deployment_root_dir, - :application_name => @application_name, - :deployment_id => @deployment_id, - :deployment_group_name => @deployment_group_name, - :deployment_group_id => @deployment_group_id, - :deployment_creator => @deployment_creator, - :deployment_type => @deployment_type, - :last_successful_deployment_dir => nil, - :most_recent_deployment_dir => nil, - :app_spec_path => 'appspec.yml'} - @hook_executor_constructor_hash_1 = hook_executor_constructor_hash.merge({:lifecycle_event => "lifecycle_event_1"}) - @hook_executor_constructor_hash_2 = hook_executor_constructor_hash.merge({:lifecycle_event => "lifecycle_event_2"}) - @mock_hook_executor = mock - end - - should "call both lifecycle events" do - HookExecutor.expects(:new).with(@hook_executor_constructor_hash_1).returns(@mock_hook_executor) - HookExecutor.expects(:new).with(@hook_executor_constructor_hash_2).returns(@mock_hook_executor) - @mock_hook_executor.expects(:execute).twice - - @command_executor.execute_command(@command, @deployment_spec) - end - - context "when the first script is forced to fail" do - setup do - HookExecutor.stubs(:new).with(@hook_executor_constructor_hash_1).raises("failed to create hook command") - end - - should "calls lifecycle event 1 and fails but not lifecycle event 2" do - assert_raised_with_message('failed to create hook command') do - @command_executor.execute_command(@command, @deployment_spec) - end - HookExecutor.expects(:new).with(@hook_executor_constructor_hash_2).never - end - end - end - end - end -end