• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

aws / aws-codedeploy-agent / 4600270359

pending completion
4600270359

push

github

GitHub
Merge pull request #350 from aws/1.5.x

88 of 88 new or added lines in 5 files covered. (100.0%)

1129 of 2362 relevant lines covered (47.8%)

2.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

32.29
/lib/instance_agent/plugins/codedeploy/hook_executor.rb
1
require 'timeout'
1✔
2
require 'open3'
1✔
3
require 'json'
1✔
4
require 'fileutils'
1✔
5

6
require 'instance_agent/plugins/codedeploy/application_specification/application_specification'
1✔
7
require 'instance_agent/platform/thread_joiner'
1✔
8
module InstanceAgent
1✔
9
  module Plugins
1✔
10
    module CodeDeployPlugin
1✔
11
      class ScriptLog
1✔
12
        SCRIPT_LOG_FILE_RELATIVE_LOCATION = 'logs/scripts.log'
1✔
13

14
        attr_reader :log
1✔
15
        def append_to_log(log_entry)
1✔
16
          log_entry ||= ""
×
17
          @log ||= []
×
18
          @log.push(log_entry)
×
19

20
          index = @log.size
×
21
          remaining_buffer = 2048
×
22

23
          while (index > 0 && (remaining_buffer - @log[index-1].length) > 0)
×
24
            index = index - 1
×
25
            remaining_buffer = remaining_buffer - @log[index-1].length
×
26
          end
27

28
          if index > 0
×
29
            @log = @log.drop(index)
×
30
          end
31
        end
32

33
        def concat_log(log_entries)
1✔
34
          log_entries ||= []
×
35
          log_entries.each do |log_entry|
×
36
            append_to_log(log_entry)
×
37
          end
38
        end
39
      end
40

41
      class ScriptError < StandardError
1✔
42
        attr_reader :error_code, :script_name, :log
1✔
43

44
        SUCCEEDED_CODE = 0
1✔
45
        SCRIPT_MISSING_CODE = 1
1✔
46
        SCRIPT_EXECUTABILITY_CODE = 2
1✔
47
        SCRIPT_TIMED_OUT_CODE = 3
1✔
48
        SCRIPT_FAILED_CODE = 4
1✔
49
        UNKNOWN_ERROR_CODE = 5
1✔
50
        OUTPUTS_LEFT_OPEN_CODE = 6
1✔
51
        FAILED_AFTER_RESTART_CODE = 7
1✔
52

53
        def initialize(error_code, script_name, log)
1✔
54
          @error_code = error_code
1✔
55
          @script_name = script_name
1✔
56
          @log = log
1✔
57
        end
58

59
        def to_json
1✔
60
          log = @log.log || []
×
61
          log = log.join("")
×
62
          log.force_encoding("utf-8")
×
63
          {'error_code' => @error_code, 'script_name' => @script_name, 'message' => message, 'log' => log}.to_json
×
64
        end
65
      end
66

67
      class HookExecutor
1✔
68
        LAST_SUCCESSFUL_DEPLOYMENT = "LastSuccessfulOrIgnore"
1✔
69
        MOST_RECENT_DEPLOYMENT = "MostRecentOrIgnore"
1✔
70
        CURRENT = "New"
1✔
71
        MAPPING_BETWEEN_HOOKS_AND_DEPLOYMENTS = { "BeforeBlockTraffic"=>LAST_SUCCESSFUL_DEPLOYMENT,
1✔
72
            "AfterBlockTraffic"=>LAST_SUCCESSFUL_DEPLOYMENT,
73
            "ApplicationStop"=>LAST_SUCCESSFUL_DEPLOYMENT,
74
            "BeforeInstall"=>CURRENT,
75
            "AfterInstall"=>CURRENT,
76
            "ApplicationStart"=>CURRENT,
77
            "BeforeAllowTraffic"=>CURRENT,
78
            "AfterAllowTraffic"=>CURRENT,
79
            "ValidateService"=>CURRENT}
80

81
        def initialize(arguments = {})
1✔
82
          #check arguments
83
          raise "Lifecycle Event Required " if arguments[:lifecycle_event].nil?
×
84
          raise "Deployment ID required " if arguments[:deployment_id].nil?
×
85
          raise "Deployment Root Directory Required " if arguments[:deployment_root_dir].nil?
×
86
          raise "App Spec Path Required " if arguments[:app_spec_path].nil?
×
87
          raise "Application name required" if arguments[:application_name].nil?
×
88
          raise "Deployment Group name required" if arguments[:deployment_group_name].nil?
×
89
          raise "Deployment creator required" if arguments[:deployment_creator].nil?
×
90
          raise "Deployment type required" if arguments[:deployment_type].nil?
×
91
          @lifecycle_event = arguments[:lifecycle_event]
×
92
          @deployment_id = arguments[:deployment_id]
×
93
          @application_name = arguments[:application_name]
×
94
          @deployment_group_name = arguments[:deployment_group_name]
×
95
          @deployment_group_id = arguments[:deployment_group_id]
×
96
          @deployment_creator = arguments[:deployment_creator]
×
97
          @deployment_type = arguments[:deployment_type]
×
98
          @current_deployment_root_dir = arguments[:deployment_root_dir]
×
99
          select_correct_deployment_root_dir(arguments[:deployment_root_dir], arguments[:last_successful_deployment_dir], arguments[:most_recent_deployment_dir])
×
100
          return if @deployment_root_dir.nil?
×
101
          @deployment_archive_dir = File.join(@deployment_root_dir, 'deployment-archive')
×
102
          @app_spec_path = arguments[:app_spec_path]
×
103
          parse_app_spec
×
104
          @hook_logging_mutex = Mutex.new
×
105
          @script_log = ScriptLog.new
×
106
          @child_envs={'LIFECYCLE_EVENT' => @lifecycle_event.to_s,
×
107
                      'DEPLOYMENT_ID'   => @deployment_id.to_s,
108
                      'APPLICATION_NAME' => @application_name,
109
                      'DEPLOYMENT_GROUP_NAME' => @deployment_group_name,
110
                      'DEPLOYMENT_GROUP_ID' => @deployment_group_id}
111
          @child_envs.merge!(arguments[:revision_envs]) if arguments[:revision_envs]
×
112
        end
113

114
        def is_noop?
1✔
115
          return @app_spec.nil? || @app_spec.hooks[@lifecycle_event].nil? || @app_spec.hooks[@lifecycle_event].empty?
×
116
        end
117

118
        def total_timeout_for_all_scripts
1✔
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

124
        def execute
1✔
125
          return if @app_spec.nil?
×
126
          if (hooks = @app_spec.hooks[@lifecycle_event]) &&
×
127
          !hooks.empty?
128
            create_script_log_file_if_needed do |script_log_file|
×
129
              log_script("LifecycleEvent - " + @lifecycle_event + "\n", script_log_file)
×
130
              hooks.each do |script|
×
131
                if(!File.exist?(script_absolute_path(script)))
×
132
                  raise ScriptError.new(ScriptError::SCRIPT_MISSING_CODE, script.location, @script_log), 'Script does not exist at specified location: ' + File.expand_path(script_absolute_path(script))
×
133
                elsif(!InstanceAgent::Platform.util.script_executable?(script_absolute_path(script)))
×
134
                  log :warn, 'Script at specified location: ' + script.location + ' is not executable.  Trying to make it executable.'
×
135
                  begin
136
                    FileUtils.chmod("+x", script_absolute_path(script))
×
137
                  rescue
138
                    raise ScriptError.new(ScriptError::SCRIPT_EXECUTABILITY_CODE, script.location, @script_log), 'Unable to set script at specified location: ' + script.location + ' as executable'
×
139
                  end
140
                end
141
                begin
142
                  execute_script(script, script_log_file)
×
143
                rescue Timeout::Error
144
                  raise ScriptError.new(ScriptError::SCRIPT_TIMED_OUT_CODE, script.location, @script_log), 'Script at specified location: ' +script.location + ' failed to complete in '+script.timeout.to_s+' seconds'
×
145
                rescue ScriptError
146
                  raise
×
147
                rescue StandardError => e
148
                  script_error = "#{script_error_prefix(script.location, script.runas)} failed with error #{e.class} with message #{e}"
×
149
                  raise ScriptError.new(ScriptError::SCRIPT_FAILED_CODE, script.location, @script_log), script_error
×
150
                end
151
              end
152
            end
153
          end
154
          @script_log.log
×
155
        end
156

157
        private
1✔
158
        def execute_script(script, script_log_file)
1✔
159
          script_command = InstanceAgent::Platform.util.prepare_script_command(script, script_absolute_path(script))
×
160
          log_script("Script - " + script.location + "\n", script_log_file)
×
161
          exit_status = 1
×
162
          signal = nil
×
163

164
          if !InstanceAgent::Platform.util.supports_process_groups?
×
165
            # The Windows port doesn't emulate process groups so don't try to use them here
166
            open3_options = {}
×
167
            signal = 'KILL' #It is up to the script to handle killing child processes it spawns.
×
168
          else
169
            open3_options = {:pgroup => true}
×
170
            signal = '-TERM' #kill the process group instead of pid
×
171
          end
172

173
          Open3.popen3(@child_envs, script_command, open3_options) do |stdin, stdout, stderr, wait_thr|
×
174
            stdin.close
×
175
            stdout_thread = Thread.new{stdout.each_line { |line| log_script("[stdout]" + line.to_s, script_log_file)}}
×
176
            stderr_thread = Thread.new{stderr.each_line { |line| log_script("[stderr]" + line.to_s, script_log_file)}}
×
177
            thread_joiner = InstanceAgent::ThreadJoiner.new(script.timeout)
×
178
            thread_joiner.joinOrFail(wait_thr) do
×
179
              Process.kill(signal, wait_thr.pid)
×
180
              raise Timeout::Error
×
181
            end
182
            thread_joiner.joinOrFail(stdout_thread) do
×
183
              script_error = "Script at specified location: #{script.location} failed to close STDOUT"
×
184
              log :error, script_error
×
185
              raise ScriptError.new(ScriptError::OUTPUTS_LEFT_OPEN_CODE, script.location, @script_log), script_error
×
186
            end
187
            thread_joiner.joinOrFail(stderr_thread) do
×
188
              script_error = "Script at specified location: #{script.location} failed to close STDERR"
×
189
              log :error, script_error
×
190
              raise ScriptError.new(ScriptError::OUTPUTS_LEFT_OPEN_CODE, script.location, @script_log), script_error
×
191
            end
192
            exit_status = wait_thr.value.exitstatus
×
193
          end
194
          if(exit_status != 0)
×
195
            script_error = "#{script_error_prefix(script.location, script.runas)} failed with exit code #{exit_status.to_s}"
×
196
            raise ScriptError.new(ScriptError::SCRIPT_FAILED_CODE, script.location, @script_log), script_error
×
197
          end
198
        end
199

200
        private
1✔
201
        def script_error_prefix(script_location, script_run_as_user)
1✔
202
          script_error_prefix = 'Script at specified location: ' + script_location
×
203
          if(!script_run_as_user.nil?)
×
204
            script_error_prefix = 'Script at specified location: ' + script_location + ' run as user ' + script_run_as_user
×
205
          end
206

207
          script_error_prefix
×
208
        end
209

210
        private
1✔
211
        def create_script_log_file_if_needed
1✔
212
          script_log_file_location = File.join(@current_deployment_root_dir, ScriptLog::SCRIPT_LOG_FILE_RELATIVE_LOCATION)
×
213
          if(!File.exists?(script_log_file_location))
×
214
            unless File.directory?(File.dirname(script_log_file_location))
×
215
              FileUtils.mkdir_p(File.dirname(script_log_file_location))
×
216
            end
217
            script_log_file = File.open(script_log_file_location, 'w')
×
218
          else
219
            script_log_file = File.open(script_log_file_location, 'a')
×
220
          end
221
          yield(script_log_file)
×
222
        ensure
223
          script_log_file.close unless script_log_file.nil?
×
224
        end
225

226
        private
1✔
227
        def script_absolute_path(script)
1✔
228
          File.join(@deployment_archive_dir, script.location)
×
229
        end
230

231
        private
1✔
232
        def parse_app_spec
1✔
233
          app_spec_location = File.join(@deployment_archive_dir, @app_spec_path)
×
234
          log(:debug, "Checking for app spec in #{app_spec_location}")
×
235
          unless File.exists?(app_spec_location)
×
236
            raise <<-MESSAGE.gsub(/^[\s\t]*/, '').gsub(/\s*\n/, ' ').strip
×
237
                The CodeDeploy agent did not find an AppSpec file within the unpacked revision directory at revision-relative path "#{@app_spec_path}".
238
                The revision was unpacked to directory "#{@deployment_archive_dir}", and the AppSpec file was expected but not found at path
239
                "#{app_spec_location}". Consult the AWS CodeDeploy Appspec documentation for more information at
240
                http://docs.aws.amazon.com/codedeploy/latest/userguide/reference-appspec-file.html
241
            MESSAGE
242
          end
243
          @app_spec =  InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::ApplicationSpecification.parse(File.read(app_spec_location))
×
244
        end
245

246
        private
1✔
247
        def select_correct_deployment_root_dir(current_deployment_root_dir, last_successful_deployment_root_dir, most_recent_deployment_dir)
1✔
248
          @deployment_root_dir = current_deployment_root_dir
×
249
          hook_deployment_mapping = mapping_between_hooks_and_deployments
×
250
          if(select_correct_mapping_for_hooks == LAST_SUCCESSFUL_DEPLOYMENT && !File.exist?(File.join(@deployment_root_dir, 'deployment-archive')))
×
251
            @deployment_root_dir = last_successful_deployment_root_dir
×
252
          elsif(select_correct_mapping_for_hooks == MOST_RECENT_DEPLOYMENT && !File.exists?(File.join(@deployment_root_dir, 'deployment-archive')))
×
253
            @deployment_root_dir = most_recent_deployment_dir
×
254
          end
255
        end
256

257
        private
1✔
258
        def select_correct_mapping_for_hooks
1✔
259
          hook_deployment_mapping = mapping_between_hooks_and_deployments
×
260
          if((@deployment_creator.eql? "codeDeployRollback") && (@deployment_type.eql? "BLUE_GREEN"))
×
261
            hook_deployment_mapping = rollback_deployment_mapping_between_hooks_and_deployments
×
262
          end
263
          hook_deployment_mapping[@lifecycle_event]
×
264
        end
265

266
        private
1✔
267
        def mapping_between_hooks_and_deployments
1✔
268
          MAPPING_BETWEEN_HOOKS_AND_DEPLOYMENTS
×
269
        end
270

271
        private 
1✔
272
        def rollback_deployment_mapping_between_hooks_and_deployments
1✔
273
          { "BeforeBlockTraffic"=>MOST_RECENT_DEPLOYMENT,
×
274
            "AfterBlockTraffic"=>MOST_RECENT_DEPLOYMENT,
275
            "ApplicationStop"=>LAST_SUCCESSFUL_DEPLOYMENT,
276
            "BeforeInstall"=>CURRENT,
277
            "AfterInstall"=>CURRENT,
278
            "ApplicationStart"=>CURRENT,
279
            "BeforeAllowTraffic"=>LAST_SUCCESSFUL_DEPLOYMENT,
280
            "AfterAllowTraffic"=>LAST_SUCCESSFUL_DEPLOYMENT,
281
            "ValidateService"=>CURRENT}
282
        end
283

284
        private
1✔
285
        def description
1✔
286
          self.class.to_s
×
287
        end
288

289
        private
1✔
290
        def log(severity, message)
1✔
291
          raise ArgumentError, "Unknown severity #{severity.inspect}" unless InstanceAgent::Log::SEVERITIES.include?(severity.to_s)
×
292
          InstanceAgent::Log.send(severity.to_sym, "#{description}: #{message}")
×
293
        end
294

295
        private
1✔
296
        def log_script(message, script_log_file)
1✔
297
          @hook_logging_mutex.synchronize do
×
298
            @script_log.append_to_log(message)
×
299
            script_log_file.write(Time.now.to_s[0..-7] + ' ' + message)
×
300
            InstanceAgent::DeploymentLog.instance.log("[#{@deployment_id}]#{message.strip}") if InstanceAgent::Config.config[:enable_deployments_log]
×
301
            script_log_file.flush
×
302
          end
303
        end
304
      end
305
    end
306
  end
307
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc