Skip to content

Commit 0efb955

Browse files
authored
Merge pull request #350 from aws/1.5.x
1.5.x
2 parents 247c7e8 + 4b4aab1 commit 0efb955

22 files changed

+3738
-191
lines changed

bin/codedeploy-agent

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
$:.unshift File.join(File.dirname(File.expand_path('..', __FILE__)), 'lib')
44

5-
ruby_versions = ["2.7", "2.6", "2.5", "2.4", "2.3", "2.2", "2.1", "2.0"]
5+
ruby_versions = ["3.0", "2.7", "2.6", "2.5", "2.4", "2.3", "2.2", "2.1", "2.0"]
66
actual_ruby_version = RUBY_VERSION.split('.').map{|s|s.to_i}
77
left_bound = '2.0.0'.split('.').map{|s|s.to_i}
88
ruby_bin = nil

bin/install

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,10 @@ EOF
213213
end
214214

215215
def supported_ruby_versions
216-
['2.7', '2.6', '2.5', '2.4', '2.3', '2.2', '2.1', '2.0']
216+
['3.0', '2.7', '2.6', '2.5', '2.4', '2.3', '2.2', '2.1', '2.0']
217217
end
218218

219-
# check ruby version, only version 2.x works
219+
# check ruby version, only version 2.x 3.x works
220220
def check_ruby_version_and_symlink
221221
@log.info("Starting Ruby version check.")
222222
actual_ruby_version = RUBY_VERSION.split('.').map{|s|s.to_i}[0,2]
@@ -241,9 +241,9 @@ EOF
241241
end
242242

243243
def unsupported_ruby_version_error
244-
@log.error("Current running Ruby version for "+ENV['USER']+" is "+RUBY_VERSION+", but Ruby version 2.x needs to be installed.")
244+
@log.error("Current running Ruby version for "+ENV['USER']+" is "+RUBY_VERSION+", but Ruby version 2.x, 3.x needs to be installed.")
245245
@log.error('If you already have the proper Ruby version installed, please either create a symlink to /usr/bin/ruby2.x,')
246-
@log.error( "or run this install script with right interpreter. Otherwise please install Ruby 2.x for "+ENV['USER']+" user.")
246+
@log.error( "or run this install script with right interpreter. Otherwise please install Ruby 2.x, 3.x for "+ENV['USER']+" user.")
247247
@log.error('You can get more information by running the script with --help option.')
248248
end
249249

@@ -295,15 +295,14 @@ EOF
295295
end
296296
@type = ARGV.shift.downcase;
297297
end
298-
299298
def force_ruby2x(ruby_interpreter_path)
300299
# change interpreter when symlink /usr/bin/ruby2.x exists, but running with non-supported ruby version
301300
actual_ruby_version = RUBY_VERSION.split('.').map{|s|s.to_i}
302301
left_bound = '2.0.0'.split('.').map{|s|s.to_i}
303-
right_bound = '2.7.0'.split('.').map{|s|s.to_i}
302+
right_bound = '3.0.0'.split('.').map{|s|s.to_i}
304303
if (actual_ruby_version <=> left_bound) < 0
305304
if(!@reexeced)
306-
@log.info("The current Ruby version is not 2.x! Restarting the installer with #{ruby_interpreter_path}")
305+
@log.info("The current Ruby version is not 2.x or 3.0.x! Restarting the installer with #{ruby_interpreter_path}")
307306
exec("#{ruby_interpreter_path}", __FILE__, '--re-execed' , *@args)
308307
else
309308
unsupported_ruby_version_error

codedeploy_agent.gemspec

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Gem::Specification.new do |spec|
22
spec.name = 'aws_codedeploy_agent'
3-
spec.version = '1.4.1'
3+
spec.version = '1.5.0'
44
spec.summary = 'Packages AWS CodeDeploy agent libraries'
55
spec.description = 'AWS CodeDeploy agent is responsible for doing the actual work of deploying software on an individual EC2 instance'
66
spec.author = 'Amazon Web Services'
@@ -17,7 +17,6 @@ Gem::Specification.new do |spec|
1717
spec.add_dependency('rubyzip', '~> 1.3.0')
1818
spec.add_dependency('logging', '~> 1.8')
1919
spec.add_dependency('aws-sdk-core', '~> 3')
20-
spec.add_dependency('aws-sdk-code-generator', '~> 0.2.2.pre')
2120
spec.add_dependency('aws-sdk-s3', '~> 1')
2221
spec.add_dependency('simple_pid', '~> 0.2.1')
2322
spec.add_dependency('docopt', '~> 0.5.0')
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
module InstanceAgent; module Plugins; module CodeDeployPlugin
2+
class CommandAcknowledgementRequestBuilder
3+
@@MIN_ACK_TIMEOUT = 60
4+
@@MAX_ACK_TIMEOUT = 4200
5+
6+
def initialize(logger)
7+
@logger = logger
8+
end
9+
10+
def build(diagnostics, host_command_identifier, timeout)
11+
result = build_default(diagnostics, host_command_identifier)
12+
if timeout && timeout > 0
13+
result[:host_command_max_duration_in_seconds] = correct_timeout(timeout)
14+
end
15+
16+
result
17+
end
18+
19+
private
20+
21+
def build_default(diagnostics, host_command_identifier)
22+
{
23+
:diagnostics => diagnostics,
24+
:host_command_identifier => host_command_identifier
25+
}
26+
end
27+
28+
def correct_timeout(timeout)
29+
result = timeout
30+
if timeout < @@MIN_ACK_TIMEOUT
31+
log(:info, "Command timeout of #{timeout} is below minimum value of #{@@MIN_ACK_TIMEOUT} " +
32+
"seconds. Sending #{@@MIN_ACK_TIMEOUT} to the service instead.")
33+
result = @@MIN_ACK_TIMEOUT
34+
elsif timeout > @@MAX_ACK_TIMEOUT
35+
log(:warn, "Command timeout of #{timeout} exceeds maximum accepted value #{@@MAX_ACK_TIMEOUT} " +
36+
"seconds. Sending #{@@MAX_ACK_TIMEOUT} to the service instead. Commands may time out.")
37+
result = @@MAX_ACK_TIMEOUT
38+
end
39+
40+
result
41+
end
42+
43+
def log(severity, message)
44+
raise ArgumentError, "Unknown severity #{severity.inspect}" unless InstanceAgent::Log::SEVERITIES.include?(severity.to_s)
45+
@logger.send(severity.to_sym, "#{self.class.to_s}: #{message}")
46+
end
47+
end end end end

lib/instance_agent/plugins/codedeploy/command_executor.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,28 @@ def is_command_noop?(command_name, deployment_spec)
7979
return true
8080
end
8181

82+
def total_timeout_for_all_lifecycle_events(command_name, deployment_spec)
83+
parsed_spec = InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification.parse(deployment_spec)
84+
timeout_sums = ((@hook_mapping || {command_name => []})[command_name] || []).map do |lifecycle_event|
85+
create_hook_executor(lifecycle_event, parsed_spec).total_timeout_for_all_scripts
86+
end
87+
88+
total_timeout = nil
89+
if timeout_sums.empty?
90+
log(:info, "Command #{command_name} has no script timeouts specified in appspec.")
91+
# If any lifecycle events' scripts don't specify a timeout, don't set a value.
92+
# The default will be the maximum at the server.
93+
elsif timeout_sums.include?(nil)
94+
log(:info, "Command #{command_name} has at least one script that does not specify a timeout. " +
95+
"No timeout override will be sent.")
96+
else
97+
total_timeout = timeout_sums.reduce(0) {|running_sum, item| running_sum + item}
98+
log(:info, "Command #{command_name} has total script timeout #{total_timeout} in appspec.")
99+
end
100+
101+
total_timeout
102+
end
103+
82104
def execute_command(command, deployment_specification)
83105
method_name = command_method(command.command_name)
84106
log(:debug, "Command #{command.command_name} maps to method #{method_name}")

lib/instance_agent/plugins/codedeploy/command_poller.rb

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,29 @@ def validate
6767
end
6868
end
6969

70+
# Called during initialization of the child process
71+
def recover_from_crash?
72+
begin
73+
if DeploymentCommandTracker.check_deployment_event_inprogress?() then
74+
log(:warn, "Deployment tracking file found: #{DeploymentCommandTracker.deployment_dir_path()}. The agent likely restarted while running a customer-supplied script. Failing the lifecycle event.")
75+
host_command_identifier = DeploymentCommandTracker.most_recent_host_command_identifier()
76+
77+
log(:info, "Calling PutHostCommandComplete: 'Failed' #{host_command_identifier}")
78+
@deploy_control_client.put_host_command_complete(
79+
:command_status => "Failed",
80+
:diagnostics => {:format => "JSON", :payload => gather_diagnostics_from_failure_after_restart("Failing in-progress lifecycle event after an agent restart.")},
81+
:host_command_identifier => host_command_identifier)
82+
83+
DeploymentCommandTracker.clean_ongoing_deployment_dir()
84+
return true
85+
end
86+
# We want to catch-all exceptions so that the child process always can startup succesfully.
87+
rescue Exception => e
88+
log(:error, "Exception thrown during restart recovery: #{e}")
89+
return nil
90+
end
91+
end
92+
7093
def perform
7194
return unless command = next_command
7295

@@ -109,7 +132,7 @@ def process_command(command, spec)
109132
log(:debug, "Calling #{@plugin.to_s}.execute_command")
110133
begin
111134
deployment_id = InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification.parse(spec).deployment_id
112-
DeploymentCommandTracker.create_ongoing_deployment_tracking_file(deployment_id)
135+
DeploymentCommandTracker.create_ongoing_deployment_tracking_file(deployment_id, command.host_command_identifier)
113136
#Successful commands will complete without raising an exception
114137
@plugin.execute_command(command, spec)
115138

@@ -232,6 +255,16 @@ def gather_diagnostics_from_error(error)
232255
gather_diagnostics_from_script_error(script_error)
233256
end
234257

258+
private
259+
def gather_diagnostics_from_failure_after_restart(msg = "")
260+
begin
261+
raise ScriptError.new(ScriptError::FAILED_AFTER_RESTART_CODE, "", ScriptLog.new), "Failed: #{msg}"
262+
rescue ScriptError => e
263+
script_error = e
264+
end
265+
gather_diagnostics_from_script_error(script_error)
266+
end
267+
235268
private
236269
def gather_diagnostics(msg = "")
237270
begin

lib/instance_agent/plugins/codedeploy/deployment_command_tracker.rb

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,21 @@ class FileDoesntExistException < Exception; end
1313
class DeploymentCommandTracker
1414
DEPLOYMENT_EVENT_FILE_STALE_TIMELIMIT_SECONDS = 86400 # 24 hour limit in secounds
1515

16-
def self.create_ongoing_deployment_tracking_file(deployment_id)
16+
def self.create_ongoing_deployment_tracking_file(deployment_id, host_command_identifier)
1717
FileUtils.mkdir_p(deployment_dir_path())
18-
FileUtils.touch(deployment_event_tracking_file_path(deployment_id));
18+
File.write(deployment_event_tracking_file_path(deployment_id), host_command_identifier)
1919
end
2020

2121
def self.delete_deployment_tracking_file_if_stale?(deployment_id, timeout)
22-
if(Time.now - File.ctime(deployment_event_tracking_file_path(deployment_id)) > timeout)
22+
if(Time.now - File.mtime(deployment_event_tracking_file_path(deployment_id)) > timeout)
2323
delete_deployment_command_tracking_file(deployment_id)
2424
return true;
2525
end
2626
return false;
2727
end
2828

2929
def self.check_deployment_event_inprogress?
30-
if(File.exists?deployment_dir_path())
30+
if(File.exist?(deployment_dir_path()))
3131
return directories_and_files_inside(deployment_dir_path()).any?{|deployment_id| check_if_lifecycle_event_is_stale?(deployment_id)}
3232
else
3333
return false
@@ -36,7 +36,7 @@ def self.check_deployment_event_inprogress?
3636

3737
def self.delete_deployment_command_tracking_file(deployment_id)
3838
ongoing_deployment_event_file_path = deployment_event_tracking_file_path(deployment_id)
39-
if File.exists?ongoing_deployment_event_file_path
39+
if File.exist?(ongoing_deployment_event_file_path)
4040
File.delete(ongoing_deployment_event_file_path);
4141
else
4242
InstanceAgent::Log.warn("the tracking file does not exist")
@@ -46,8 +46,18 @@ def self.delete_deployment_command_tracking_file(deployment_id)
4646
def self.directories_and_files_inside(directory)
4747
Dir.entries(directory) - %w(.. .)
4848
end
49-
50-
private
49+
50+
def self.most_recent_host_command_identifier
51+
# check_deployment_event_inprogress handles deleting stale files for us.
52+
if check_deployment_event_inprogress? then
53+
most_recent_id = directories_and_files_inside(deployment_dir_path()).max_by{ |filename| File.mtime(deployment_event_tracking_file_path(filename)) }
54+
most_recent_file = deployment_event_tracking_file_path(most_recent_id)
55+
return File.read(most_recent_file)
56+
else
57+
return nil
58+
end
59+
end
60+
5161
def self.deployment_dir_path
5262
File.join(InstanceAgent::Config.config[:root_dir], InstanceAgent::Config.config[:ongoing_deployment_tracking])
5363
end
@@ -57,8 +67,12 @@ def self.check_if_lifecycle_event_is_stale?(deployment_id)
5767
end
5868

5969
def self.deployment_event_tracking_file_path(deployment_id)
60-
ongoing_deployment_file_path = File.join(deployment_dir_path(), deployment_id)
61-
end
70+
return File.join(deployment_dir_path(), deployment_id)
71+
end
72+
73+
def self.clean_ongoing_deployment_dir
74+
FileUtils.rm_r(InstanceAgent::Plugins::CodeDeployPlugin::DeploymentCommandTracker.deployment_dir_path()) rescue Errno::ENOENT
75+
end
6276
end
6377
end
6478
end

lib/instance_agent/plugins/codedeploy/hook_executor.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ class ScriptError < StandardError
4848
SCRIPT_FAILED_CODE = 4
4949
UNKNOWN_ERROR_CODE = 5
5050
OUTPUTS_LEFT_OPEN_CODE = 6
51+
FAILED_AFTER_RESTART_CODE = 7
52+
5153
def initialize(error_code, script_name, log)
5254
@error_code = error_code
5355
@script_name = script_name
@@ -113,6 +115,12 @@ def is_noop?
113115
return @app_spec.nil? || @app_spec.hooks[@lifecycle_event].nil? || @app_spec.hooks[@lifecycle_event].empty?
114116
end
115117

118+
def total_timeout_for_all_scripts
119+
return nil if is_noop?
120+
timeouts = @app_spec.hooks[@lifecycle_event].map {|script| script.timeout}
121+
timeouts.reduce(0) {|running_sum, item| running_sum + item}
122+
end
123+
116124
def execute
117125
return if @app_spec.nil?
118126
if (hooks = @app_spec.hooks[@lifecycle_event]) &&

lib/instance_agent/runner/child.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class Child < ProcessManager::Daemon::Child
88

99
attr_accessor :runner
1010

11+
@prepare_run_done = false
12+
1113
def load_plugins(plugins)
1214
ProcessManager::Log.debug("Registering Plugins: #{plugins.inspect}.")
1315
plugins.each do |plugin|
@@ -31,24 +33,30 @@ def prepare_run
3133
with_error_handling do
3234
@runner = @plugins[index].runner
3335
ProcessManager.set_program_name(description)
36+
@runner.recover_from_crash?()
3437
end
38+
39+
@prepare_run_done = true
3540
end
3641

3742
def run
3843
with_error_handling do
3944
runner.run
4045
end
4146
end
42-
47+
4348
# Stops the master after recieving the kill signal
44-
# is overriden from ProcessManager::Daemon::Child
49+
# is overriden from ProcessManager::Daemon::Child
4550
def stop
46-
@runner.graceful_shutdown
51+
if @prepare_run_done
52+
@runner.graceful_shutdown
53+
end
54+
4755
ProcessManager::Log.info('agent exiting now')
4856
super
4957
end
5058

51-
# Catches the trap signals and does a default or custom action
59+
# Catches the trap signals and does a default or custom action
5260
# is overriden from ProcessManager::Daemon::Child
5361
def trap_signals
5462
[:INT, :QUIT, :TERM].each do |sig|

lib/winagent.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def service_main
4141
begin
4242
@polling_mutex.synchronize do
4343
@runner ||= InstanceAgent::Plugins::CodeDeployPlugin::CommandPoller.runner
44+
@runner.recover_from_crash?
4445
@runner.run
4546
end
4647
rescue SystemExit

0 commit comments

Comments
 (0)