aws-codedeploy-agent 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. data/aws-codedeploy-agent.gemspec +5 -5
  2. data/certs/host-agent-deployment-signer-ca-chain.pem +30 -0
  3. data/conf/codedeployagent.yml +0 -1
  4. data/lib/instance_agent.rb +1 -13
  5. data/lib/instance_agent/agent/base.rb +38 -12
  6. data/lib/instance_agent/agent/plugin.rb +21 -0
  7. data/lib/instance_agent/config.rb +2 -1
  8. data/lib/instance_agent/platform/linux_util.rb +4 -0
  9. data/lib/instance_agent/plugins/codedeploy/application_specification/ace_info.rb +133 -0
  10. data/lib/instance_agent/plugins/codedeploy/application_specification/acl_info.rb +163 -0
  11. data/lib/instance_agent/plugins/codedeploy/application_specification/application_specification.rb +143 -0
  12. data/lib/instance_agent/plugins/codedeploy/application_specification/context_info.rb +23 -0
  13. data/lib/instance_agent/plugins/codedeploy/application_specification/file_info.rb +23 -0
  14. data/lib/instance_agent/plugins/codedeploy/application_specification/linux_permission_info.rb +121 -0
  15. data/lib/instance_agent/plugins/codedeploy/application_specification/mode_info.rb +66 -0
  16. data/lib/instance_agent/plugins/codedeploy/application_specification/range_info.rb +134 -0
  17. data/lib/instance_agent/plugins/codedeploy/application_specification/script_info.rb +27 -0
  18. data/lib/instance_agent/plugins/codedeploy/codedeploy_control.rb +100 -0
  19. data/lib/instance_agent/plugins/codedeploy/command_executor.rb +359 -0
  20. data/lib/instance_agent/plugins/codedeploy/command_poller.rb +178 -0
  21. data/lib/instance_agent/plugins/codedeploy/deployment_specification.rb +161 -0
  22. data/lib/instance_agent/plugins/codedeploy/hook_executor.rb +226 -0
  23. data/lib/instance_agent/plugins/codedeploy/install_instruction.rb +389 -0
  24. data/lib/instance_agent/plugins/codedeploy/installer.rb +147 -0
  25. data/lib/instance_agent/plugins/codedeploy/onpremise_config.rb +42 -0
  26. data/lib/instance_agent/plugins/codedeploy/register_plugin.rb +17 -0
  27. data/lib/instance_agent/runner/child.rb +20 -5
  28. data/lib/instance_agent/runner/master.rb +2 -15
  29. data/lib/instance_metadata.rb +2 -2
  30. data/test/certificate_helper.rb +1 -1
  31. data/test/helpers/instance_agent_helper.rb +1 -0
  32. data/test/instance_agent/agent/base_test.rb +16 -3
  33. data/test/instance_agent/config_test.rb +2 -1
  34. data/test/instance_agent/plugins/codedeploy/application_specification_test.rb +1713 -0
  35. data/test/instance_agent/{codedeploy_plugin → plugins/codedeploy}/codedeploy_control_test.rb +1 -1
  36. data/test/instance_agent/{codedeploy_plugin → plugins/codedeploy}/command_executor_test.rb +32 -9
  37. data/test/instance_agent/{codedeploy_plugin → plugins/codedeploy}/command_poller_test.rb +13 -14
  38. data/test/instance_agent/{codedeploy_plugin → plugins/codedeploy}/deployment_specification_test.rb +98 -25
  39. data/test/instance_agent/{codedeploy_plugin → plugins/codedeploy}/hook_executor_test.rb +83 -15
  40. data/test/instance_agent/plugins/codedeploy/install_instruction_test.rb +568 -0
  41. data/test/instance_agent/{codedeploy_plugin → plugins/codedeploy}/installer_test.rb +12 -9
  42. data/test/instance_agent/plugins/codedeploy/onpremise_config_test.rb +72 -0
  43. data/test/instance_agent/runner/child_test.rb +1 -1
  44. data/vendor/gems/.codedeploy-commands-1.0.0.created.rid +1 -1
  45. data/vendor/gems/codedeploy-commands/lib/aws/plugins/deploy_control_endpoint.rb +4 -0
  46. data/vendor/gems/jmespath-1.0.1/lib/jmespath.rb +41 -0
  47. data/vendor/gems/jmespath-1.0.1/lib/jmespath/caching_parser.rb +30 -0
  48. data/vendor/gems/jmespath-1.0.1/lib/jmespath/errors.rb +17 -0
  49. data/vendor/gems/jmespath-1.0.1/lib/jmespath/expr_node.rb +15 -0
  50. data/vendor/gems/jmespath-1.0.1/lib/jmespath/lexer.rb +116 -0
  51. data/vendor/gems/jmespath-1.0.1/lib/jmespath/parser.rb +347 -0
  52. data/vendor/gems/jmespath-1.0.1/lib/jmespath/runtime.rb +71 -0
  53. data/vendor/gems/jmespath-1.0.1/lib/jmespath/token.rb +41 -0
  54. data/vendor/gems/jmespath-1.0.1/lib/jmespath/token_stream.rb +60 -0
  55. data/vendor/gems/jmespath-1.0.1/lib/jmespath/tree_interpreter.rb +523 -0
  56. data/vendor/gems/jmespath-1.0.1/lib/jmespath/version.rb +3 -0
  57. data/vendor/gems/process_manager/lib/process_manager/master.rb +16 -5
  58. data/vendor/specifications/{aws-sdk-core-2.0.5.gemspec → aws-sdk-core-2.0.42.gemspec} +9 -11
  59. data/vendor/specifications/builder-3.2.2.gemspec +1 -1
  60. data/vendor/specifications/codedeploy-commands-1.0.0.gemspec +7 -6
  61. data/vendor/specifications/gli-2.5.6.gemspec +1 -1
  62. data/vendor/specifications/jmespath-1.0.1.gemspec +29 -0
  63. data/vendor/specifications/little-plugger-1.1.3.gemspec +1 -1
  64. data/vendor/specifications/logging-1.8.1.gemspec +1 -1
  65. data/vendor/specifications/multi_json-1.7.7.gemspec +1 -1
  66. data/vendor/specifications/multi_json-1.8.4.gemspec +1 -1
  67. data/vendor/specifications/multi_xml-0.5.5.gemspec +1 -1
  68. data/vendor/specifications/process_manager-0.0.13.gemspec +1 -1
  69. data/vendor/specifications/simple_pid-0.2.1.gemspec +1 -1
  70. metadata +76 -63
  71. data/lib/instance_agent/codedeploy_plugin/application_specification/ace_info.rb +0 -133
  72. data/lib/instance_agent/codedeploy_plugin/application_specification/acl_info.rb +0 -163
  73. data/lib/instance_agent/codedeploy_plugin/application_specification/application_specification.rb +0 -142
  74. data/lib/instance_agent/codedeploy_plugin/application_specification/context_info.rb +0 -23
  75. data/lib/instance_agent/codedeploy_plugin/application_specification/file_info.rb +0 -23
  76. data/lib/instance_agent/codedeploy_plugin/application_specification/linux_permission_info.rb +0 -121
  77. data/lib/instance_agent/codedeploy_plugin/application_specification/mode_info.rb +0 -66
  78. data/lib/instance_agent/codedeploy_plugin/application_specification/range_info.rb +0 -134
  79. data/lib/instance_agent/codedeploy_plugin/application_specification/script_info.rb +0 -27
  80. data/lib/instance_agent/codedeploy_plugin/codedeploy_control.rb +0 -72
  81. data/lib/instance_agent/codedeploy_plugin/command_executor.rb +0 -357
  82. data/lib/instance_agent/codedeploy_plugin/command_poller.rb +0 -170
  83. data/lib/instance_agent/codedeploy_plugin/deployment_specification.rb +0 -150
  84. data/lib/instance_agent/codedeploy_plugin/hook_executor.rb +0 -206
  85. data/lib/instance_agent/codedeploy_plugin/install_instruction.rb +0 -374
  86. data/lib/instance_agent/codedeploy_plugin/installer.rb +0 -143
  87. data/lib/instance_agent/codedeploy_plugin/request_helper.rb +0 -28
  88. data/test/instance_agent/codedeploy_plugin/application_specification_test.rb +0 -1710
  89. data/test/instance_agent/codedeploy_plugin/install_instruction_test.rb +0 -566
  90. data/test/instance_agent/codedeploy_plugin/request_helper_test.rb +0 -37
  91. data/vendor/specifications/jamespath-0.5.1.gemspec +0 -35
@@ -0,0 +1,178 @@
1
+ require 'instance_metadata'
2
+ require 'socket'
3
+
4
+ module InstanceAgent
5
+ module Plugins
6
+ module CodeDeployPlugin
7
+ class CommandPoller < InstanceAgent::Agent::Base
8
+
9
+ VERSION = "2013-04-23"
10
+ def initialize
11
+ CodeDeployPlugin::OnPremisesConfig.configure
12
+ region = ENV['AWS_REGION'] || InstanceMetadata.region
13
+ @host_identifier = ENV['AWS_HOST_IDENTIFIER'] || InstanceMetadata.host_identifier
14
+
15
+ log(:debug, "Configuring deploy control client: Region = #{region.inspect}")
16
+ log(:debug, "Deploy control endpoint override = " + ENV['AWS_DEPLOY_CONTROL_ENDPOINT'].inspect)
17
+
18
+ @deploy_control = InstanceAgent::Plugins::CodeDeployPlugin::CodeDeployControl.new(:region => region, :logger => InstanceAgent::Log, :ssl_ca_directory => ENV['AWS_SSL_CA_DIRECTORY'])
19
+ @deploy_control_client = @deploy_control.get_client
20
+
21
+ @plugin = InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor.new(:hook_mapping => create_hook_mapping)
22
+
23
+ log(:debug, "Initializing Host Agent: " +
24
+ "Host Identifier = #{@host_identifier}")
25
+ end
26
+
27
+ def create_hook_mapping
28
+ #Map commands to lifecycle hooks
29
+ { "BeforeELBRemove"=>["BeforeELBRemove"],
30
+ "AfterELBRemove"=>["AfterELBRemove"],
31
+ "ApplicationStop"=>["ApplicationStop"],
32
+ "BeforeInstall"=>["BeforeInstall"],
33
+ "AfterInstall"=>["AfterInstall"],
34
+ "ApplicationStart"=>["ApplicationStart"],
35
+ "BeforeELBAdd"=>["BeforeELBAdd"],
36
+ "AfterELBAdd"=>["AfterELBAdd"],
37
+ "ValidateService"=>["ValidateService"]}
38
+ end
39
+
40
+ def validate
41
+ test_profile = InstanceAgent::Config.config[:codedeploy_test_profile]
42
+ unless ["beta", "gamma"].include?(test_profile.downcase)
43
+ log(:debug, "Validating CodeDeploy Plugin Configuration")
44
+ Kernel.abort "Stopping CodeDeploy agent due to SSL validation error." unless @deploy_control.validate_ssl_config
45
+ log(:debug, "CodeDeploy Plugin Configuration is valid")
46
+ end
47
+ end
48
+
49
+ def perform
50
+ return unless command = next_command
51
+ return unless acknowledge_command(command)
52
+
53
+ begin
54
+ spec = get_deployment_specification(command)
55
+ #Successful commands will complete without raising an exception
56
+ script_output = process_command(command, spec)
57
+ log(:debug, 'Calling PutHostCommandComplete: "Succeeded"')
58
+ @deploy_control_client.put_host_command_complete(
59
+ :command_status => 'Succeeded',
60
+ :diagnostics => {:format => "JSON", :payload => gather_diagnostics()},
61
+ :host_command_identifier => command.host_command_identifier)
62
+
63
+ #Commands that throw an exception will be considered to have failed
64
+ rescue ScriptError => e
65
+ log(:debug, 'Calling PutHostCommandComplete: "Code Error" ')
66
+ @deploy_control_client.put_host_command_complete(
67
+ :command_status => "Failed",
68
+ :diagnostics => {:format => "JSON", :payload => gather_diagnostics_from_script_error(e)},
69
+ :host_command_identifier => command.host_command_identifier)
70
+ raise e
71
+ rescue Exception => e
72
+ log(:debug, 'Calling PutHostCommandComplete: "Code Error" ')
73
+ @deploy_control_client.put_host_command_complete(
74
+ :command_status => "Failed",
75
+ :diagnostics => {:format => "JSON", :payload => gather_diagnostics_from_error(e)},
76
+ :host_command_identifier => command.host_command_identifier)
77
+ raise e
78
+ end
79
+ end
80
+
81
+ def next_command
82
+ log(:debug, "Calling PollHostCommand:")
83
+ output = @deploy_control_client.poll_host_command(:host_identifier => @host_identifier)
84
+ command = output.host_command
85
+ if command.nil?
86
+ log(:debug, "PollHostCommand: Host Command = nil")
87
+ else
88
+ log(:debug, "PollHostCommand: " +
89
+ "Host Identifier = #{command.host_identifier}; " +
90
+ "Host Command Identifier = #{command.host_command_identifier}; " +
91
+ "Deployment Execution ID = #{command.deployment_execution_id}; " +
92
+ "Command Name = #{command.command_name}")
93
+ raise "Host Identifier mismatch: #{@host_identifier} != #{command.host_identifier}" unless @host_identifier.include? command.host_identifier
94
+ raise "Command Name missing" if command.command_name.nil? || command.command_name.empty?
95
+ end
96
+ command
97
+ end
98
+
99
+ def acknowledge_command(command)
100
+ log(:debug, "Calling PutHostCommandAcknowledgement:")
101
+ output = @deploy_control_client.put_host_command_acknowledgement(
102
+ :diagnostics => nil,
103
+ :host_command_identifier => command.host_command_identifier)
104
+ status = output.command_status
105
+ log(:debug, "Command Status = #{status}")
106
+
107
+ if status == 'Succeeded' || status == 'Failed'
108
+ log(:debug, "Calling PutHostCommandComplete: \"#{status}\" ")
109
+ @deploy_control_client.put_host_command_complete(
110
+ :command_status => status,
111
+ :diagnostics => {:format => "JSON", :payload => gather_diagnostics_from_acknowledge(status)},
112
+ :host_command_identifier => command.host_command_identifier)
113
+ return false
114
+ end
115
+
116
+ return true
117
+ end
118
+
119
+ def get_deployment_specification(command)
120
+ log(:debug, "Calling GetDeploymentSpecification:")
121
+ output = @deploy_control_client.get_deployment_specification(
122
+ :deployment_execution_id => command.deployment_execution_id,
123
+ :host_identifier => @host_identifier)
124
+ log(:debug, "GetDeploymentSpecification: " +
125
+ "Deployment System = #{output.deployment_system}")
126
+ raise "Deployment System mismatch: #{@plugin.deployment_system} != #{output.deployment_system}" unless @plugin.deployment_system == output.deployment_system
127
+ raise "Deployment Specification missing" if output.deployment_specification.nil?
128
+ output.deployment_specification.generic_envelope
129
+ end
130
+
131
+ def process_command(command, spec)
132
+ log(:debug, "Calling #{@plugin.to_s}.execute_command")
133
+ @plugin.execute_command(command, spec)
134
+ end
135
+
136
+ private
137
+ def gather_diagnostics_from_script_error(script_error)
138
+ script_error.to_json
139
+ end
140
+
141
+ private
142
+ def gather_diagnostics_from_error(error)
143
+ begin
144
+ message = error.message || ""
145
+ raise ScriptError.new(ScriptError::UNKNOWN_ERROR_CODE, "", ScriptLog.new), message
146
+ rescue ScriptError => e
147
+ script_error = e
148
+ end
149
+ gather_diagnostics_from_script_error(script_error)
150
+ end
151
+
152
+ private
153
+ def gather_diagnostics()
154
+ begin
155
+ raise ScriptError.new(ScriptError::SUCCEEDED_CODE, "", ScriptLog.new), 'Succeeded'
156
+ rescue ScriptError => e
157
+ script_error = e
158
+ end
159
+ gather_diagnostics_from_script_error(script_error)
160
+ end
161
+
162
+ private
163
+ def gather_diagnostics_from_acknowledge(status)
164
+ begin
165
+ if status == 'Succeeded'
166
+ raise ScriptError.new(ScriptError::SUCCEEDED_CODE, "", ScriptLog.new), 'Succeeded'
167
+ else
168
+ raise ScriptError.new(ScriptError::UNKNOWN_ERROR_CODE, "", ScriptLog.new), 'Failed'
169
+ end
170
+ rescue ScriptError => e
171
+ script_error = e
172
+ end
173
+ gather_diagnostics_from_script_error(script_error)
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,161 @@
1
+ require 'openssl'
2
+ require 'instance_metadata'
3
+
4
+ module InstanceAgent
5
+ module Plugins
6
+ module CodeDeployPlugin
7
+ class DeploymentSpecification
8
+ attr_accessor :deployment_id, :deployment_group_id, :deployment_group_name, :revision, :revision_source, :application_name
9
+ attr_accessor :bucket, :key, :bundle_type, :version, :etag
10
+ attr_accessor :external_account, :repository, :commit_id, :anonymous, :external_auth_token
11
+ class << self
12
+ attr_accessor :cert_store
13
+ end
14
+
15
+ def self.init_cert_store(ca_chain_path)
16
+ @cert_store = OpenSSL::X509::Store.new
17
+ begin
18
+ @cert_store.add_file ca_chain_path
19
+ rescue OpenSSL::X509::StoreError => e
20
+ raise "Could not load certificate store '#{ca_chain_path}'.\nCaused by: #{e.inspect}"
21
+ end
22
+ return @cert_store
23
+ end
24
+
25
+ @cert_store = init_cert_store(File.expand_path('../../../../certs/host-agent-deployment-signer-ca-chain.pem', File.dirname(__FILE__)))
26
+
27
+ def initialize(data)
28
+ raise 'Deployment Spec has no DeploymentId' unless property_set?(data, "DeploymentId")
29
+ raise 'Deployment Spec has no DeploymentGroupId' unless property_set?(data, "DeploymentGroupId")
30
+ raise 'Deployment Spec has no DeploymentGroupName' unless property_set?(data, "DeploymentGroupName")
31
+ raise 'Deployment Spec has no ApplicationName' unless property_set?(data, "ApplicationName")
32
+
33
+ @application_name = data["ApplicationName"]
34
+ @deployment_group_name = data["DeploymentGroupName"]
35
+
36
+ if data["DeploymentId"].start_with?("arn:")
37
+ @deployment_id = getDeploymentIdFromArn(data["DeploymentId"])
38
+ else
39
+ @deployment_id = data["DeploymentId"]
40
+ end
41
+ @deployment_group_id = data["DeploymentGroupId"]
42
+
43
+ raise 'Must specify a revison' unless data["Revision"]
44
+ @revision_source = data["Revision"]["RevisionType"]
45
+ raise 'Must specify a revision source' unless @revision_source
46
+
47
+ case @revision_source
48
+ when 'S3'
49
+ @revision = data["Revision"]["S3Revision"]
50
+ raise 'S3Revision in Deployment Spec must specify Bucket, Key and BundleType' unless valid_s3_revision?(@revision)
51
+ raise 'BundleType in S3Revision must be tar, tgz or zip' unless valid_bundle_type?(@revision)
52
+
53
+ @bucket = @revision["Bucket"]
54
+ @key = @revision["Key"]
55
+ @bundle_type = @revision["BundleType"]
56
+ @version = @revision["Version"]
57
+ @etag = @revision["ETag"]
58
+ when 'GitHub'
59
+ @revision = data["Revision"]["GitHubRevision"]
60
+ raise 'GitHubRevision in Deployment Spec must specify Account, Repository and CommitId' unless valid_github_revision?(revision)
61
+ @external_account = revision["Account"]
62
+ @repository = revision["Repository"]
63
+ @commit_id = revision["CommitId"]
64
+ @external_auth_token = data["GitHubAccessToken"]
65
+ @anonymous = @external_auth_token.nil?
66
+ else
67
+ raise 'Exactly one of S3Revision or GitHubRevision must be specified'
68
+ end
69
+ end
70
+
71
+ def self.parse(envelope)
72
+ raise 'Provided deployment spec was nil' if envelope.nil?
73
+
74
+ case envelope.format
75
+ when "PKCS7/JSON"
76
+ pkcs7 = OpenSSL::PKCS7.new(envelope.payload)
77
+
78
+ # The PKCS7_NOCHAIN flag tells OpenSSL to ignore any PKCS7 CA chain that might be attached
79
+ # to the message directly and use the certificates from provided one only for validating the.
80
+ # signer's certificate.
81
+ #
82
+ # However, it will allow use the PKCS7 signer certificate provided to validate the signature.
83
+ #
84
+ # http://www.openssl.org/docs/crypto/PKCS7_verify.html#VERIFY_PROCESS
85
+ #
86
+ # The ruby wrapper returns true if OpenSSL returns 1
87
+ raise "Validation of PKCS7 signed message failed" unless pkcs7.verify([], @cert_store, nil, OpenSSL::PKCS7::NOCHAIN)
88
+
89
+ signer_certs = pkcs7.certificates
90
+ raise "Validation of PKCS7 signed message failed" unless signer_certs.size == 1
91
+ raise "Validation of PKCS7 signed message failed" unless verify_pkcs7_signer_cert(signer_certs[0])
92
+
93
+ deployment_spec = JSON.parse(pkcs7.data)
94
+
95
+ sanitized_spec = deployment_spec.clone
96
+ sanitized_spec["GitHubAccessToken"] &&= "REDACTED"
97
+ InstanceAgent::Log.debug("#{self.to_s}: Parse: #{sanitized_spec}")
98
+
99
+ return new(deployment_spec)
100
+ else
101
+ raise "Unsupported DeploymentSpecification format: #{envelope.format}"
102
+ end
103
+ end
104
+
105
+ private
106
+ def property_set?(propertyHash, property)
107
+ propertyHash.has_key?(property) && !propertyHash[property].nil? && !propertyHash[property].empty?
108
+ end
109
+
110
+ def valid_s3_revision?(revision)
111
+ revision.nil? || %w(Bucket Key BundleType).all? { |k| revision.has_key?(k) }
112
+ end
113
+
114
+ def valid_github_revision?(revision)
115
+ required_fields = %w(Account Repository CommitId)
116
+ if !(revision.nil? || revision['Anonymous'].nil? || revision['Anonymous'])
117
+ required_fields << 'OAuthToken'
118
+ end
119
+ revision.nil? || required_fields.all? { |k| revision.has_key?(k) }
120
+ end
121
+
122
+ private
123
+ def valid_bundle_type?(revision)
124
+ revision.nil? || %w(tar zip tgz).any? { |k| revision["BundleType"] == k }
125
+ end
126
+
127
+ def self.verify_pkcs7_signer_cert(cert)
128
+ @@region ||= ENV['AWS_REGION'] || InstanceMetadata.region
129
+
130
+ # Do some minimal cert pinning
131
+ case InstanceAgent::Config.config()[:codedeploy_test_profile]
132
+ when 'beta', 'gamma'
133
+ cert.subject.to_s == "/C=US/ST=Washington/L=Seattle/O=Amazon.com, Inc./CN=codedeploy-signer-integ.amazonaws.com"
134
+ when 'prod'
135
+ case @@region
136
+ when 'us-east-1'
137
+ cert.subject.to_s == "/C=US/ST=Washington/L=Seattle/O=Amazon.com, Inc./CN=codedeploy-signer-us-east-1.amazonaws.com"
138
+ when 'us-west-2'
139
+ cert.subject.to_s == "/C=US/ST=Washington/L=Seattle/O=Amazon.com, Inc./CN=codedeploy-signer-us-west-2.amazonaws.com"
140
+ when 'eu-west-1'
141
+ cert.subject.to_s == "/C=US/ST=Washington/L=Seattle/O=Amazon.com, Inc./CN=codedeploy-signer-eu-west-1.amazonaws.com"
142
+ when 'ap-southeast-2'
143
+ cert.subject.to_s == "/C=US/ST=Washington/L=Seattle/O=Amazon.com, Inc./CN=codedeploy-signer-ap-southeast-2.amazonaws.com"
144
+ else
145
+ raise "Unknown region '#{@region}'"
146
+ end
147
+ else
148
+ raise "Unknown profile '#{Config.config()[:codedeploy_test_profile]}'"
149
+ end
150
+ end
151
+
152
+ private
153
+ def getDeploymentIdFromArn(arn)
154
+ # example arn format: "arn:aws:codedeploy:us-east-1:123412341234:deployment/12341234-1234-1234-1234-123412341234"
155
+ arn.split(":", 6)[5].split("/",2)[1]
156
+ end
157
+
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,226 @@
1
+ require 'timeout'
2
+ require 'open3'
3
+ require 'json'
4
+ require 'fileutils'
5
+
6
+ module InstanceAgent
7
+ module Plugins
8
+ module CodeDeployPlugin
9
+ class ScriptLog
10
+ attr_reader :log
11
+ def append_to_log(log_entry)
12
+ log_entry ||= ""
13
+ @log ||= []
14
+ @log.push(log_entry)
15
+
16
+ index = @log.size
17
+ remaining_buffer = 2048
18
+
19
+ while (index > 0 && (remaining_buffer - @log[index-1].length) > 0)
20
+ index = index - 1
21
+ remaining_buffer = remaining_buffer - @log[index-1].length
22
+ end
23
+
24
+ if index > 0
25
+ @log = @log.drop(index)
26
+ end
27
+ end
28
+
29
+ def concat_log(log_entries)
30
+ log_entries ||= []
31
+ log_entries.each do |log_entry|
32
+ append_to_log(log_entry)
33
+ end
34
+ end
35
+ end
36
+
37
+ class ScriptError < StandardError
38
+ attr_reader :error_code, :script_name, :log
39
+
40
+ SUCCEEDED_CODE = 0
41
+ SCRIPT_MISSING_CODE = 1
42
+ SCRIPT_EXECUTABILITY_CODE = 2
43
+ SCRIPT_TIMED_OUT_CODE = 3
44
+ SCRIPT_FAILED_CODE = 4
45
+ UNKNOWN_ERROR_CODE = 5
46
+ def initialize(error_code, script_name, log)
47
+ @error_code = error_code
48
+ @script_name = script_name
49
+ @log = log
50
+ end
51
+
52
+ def to_json
53
+ log = @log.log || []
54
+ log = log.join("")
55
+ {'error_code' => @error_code, 'script_name' => @script_name, 'message' => message, 'log' => log}.to_json
56
+ end
57
+ end
58
+
59
+ class HookExecutor
60
+
61
+ LAST_SUCCESSFUL_DEPLOYMENT = "OldOrIgnore"
62
+ CURRENT = "New"
63
+ def initialize(arguments = {})
64
+ #check arguments
65
+ raise "Lifecycle Event Required " if arguments[:lifecycle_event].nil?
66
+ raise "Deployment ID required " if arguments[:deployment_id].nil?
67
+ raise "Deployment Root Directory Required " if arguments[:deployment_root_dir].nil?
68
+ raise "App Spec Path Required " if arguments[:app_spec_path].nil?
69
+ raise "Application name required" if arguments[:application_name].nil?
70
+ raise "Deployment Group name required" if arguments[:deployment_group_name].nil?
71
+ @lifecycle_event = arguments[:lifecycle_event]
72
+ @deployment_id = arguments[:deployment_id]
73
+ @application_name = arguments[:application_name]
74
+ @deployment_group_name = arguments[:deployment_group_name]
75
+ select_correct_deployment_root_dir(arguments[:deployment_root_dir], arguments[:last_successful_deployment_dir])
76
+ return if @deployment_root_dir.nil?
77
+ @deployment_archive_dir = File.join(@deployment_root_dir, 'deployment-archive')
78
+ @app_spec_path = arguments[:app_spec_path]
79
+ parse_app_spec
80
+ @hook_logging_mutex = Mutex.new
81
+ @script_log = ScriptLog.new
82
+ @child_envs={'LIFECYCLE_EVENT' => @lifecycle_event.to_s,
83
+ 'DEPLOYMENT_ID' => @deployment_id.to_s,
84
+ 'APPLICATION_NAME' => @application_name,
85
+ 'DEPLOYMENT_GROUP_NAME' => @deployment_group_name}
86
+
87
+ end
88
+
89
+ def execute
90
+ return if @app_spec.nil?
91
+ if (hooks = @app_spec.hooks[@lifecycle_event]) &&
92
+ !hooks.empty?
93
+ create_script_log_file_if_needed do |script_log_file|
94
+ log_script("LifecycleEvent - " + @lifecycle_event + "\n", script_log_file)
95
+ hooks.each do |script|
96
+ if(!File.exist?(script_absolute_path(script)))
97
+ raise ScriptError.new(ScriptError::SCRIPT_MISSING_CODE, script.location, @script_log), 'Script does not exist at specified location: ' + script.location
98
+ elsif(!InstanceAgent::Platform.util.script_executable?(script_absolute_path(script)))
99
+ log :warn, 'Script at specified location: ' + script.location + ' is not executable. Trying to make it executable.'
100
+ begin
101
+ FileUtils.chmod("+x", script_absolute_path(script))
102
+ rescue
103
+ raise ScriptError.new(ScriptError::SCRIPT_EXECUTABILITY_CODE, script.location, @script_log), 'Unable to set script at specified location: ' + script.location + ' as executable'
104
+ end
105
+ end
106
+ begin
107
+ execute_script(script, script_log_file)
108
+ rescue Timeout::Error
109
+ 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'
110
+ end
111
+ end
112
+ end
113
+ end
114
+ @script_log.log
115
+ end
116
+
117
+ private
118
+ def execute_script(script, script_log_file)
119
+ script_command = InstanceAgent::Platform.util.prepare_script_command(script, script_absolute_path(script))
120
+ log_script("Script - " + script.location + "\n", script_log_file)
121
+ exit_status = 1
122
+ signal = nil
123
+
124
+ if !InstanceAgent::Platform.util.supports_process_groups?
125
+ # The Windows port doesn't emulate process groups so don't try to use them here
126
+ open3_options = {}
127
+ signal = 'KILL' #It is up to the script to handle killing child processes it spawns.
128
+ else
129
+ open3_options = {:pgroup => true}
130
+ signal = '-TERM' #kill the process group instead of pid
131
+ end
132
+
133
+ Open3.popen3(@child_envs, script_command, open3_options) do |stdin, stdout, stderr, wait_thr|
134
+ stdin.close
135
+ stdout_thread = Thread.new{stdout.each_line { |line| log_script("[stdout]" + line.to_s, script_log_file)}}
136
+ stderr_thread = Thread.new{stderr.each_line { |line| log_script("[stderr]" + line.to_s, script_log_file)}}
137
+ if !wait_thr.join(script.timeout)
138
+ Process.kill(signal, wait_thr.pid)
139
+ raise Timeout::Error
140
+ end
141
+ stdout_thread.join
142
+ stderr_thread.join
143
+ exit_status = wait_thr.value.exitstatus
144
+ end
145
+ if(exit_status != 0)
146
+ script_error = 'Script at specified location: ' + script.location + ' failed with exit code ' + exit_status.to_s
147
+ if(!script.runas.nil?)
148
+ script_error = 'Script at specified location: ' + script.location + ' run as user ' + script.runas + ' failed with exit code ' + exit_status.to_s
149
+ end
150
+ raise ScriptError.new(ScriptError::SCRIPT_FAILED_CODE, script.location, @script_log), script_error
151
+ end
152
+ end
153
+
154
+ private
155
+ def create_script_log_file_if_needed
156
+ script_log_file_location = File.join(@deployment_root_dir, 'logs/scripts.log')
157
+ if(!File.exists?(script_log_file_location))
158
+ unless File.directory?(File.dirname(script_log_file_location))
159
+ FileUtils.mkdir_p(File.dirname(script_log_file_location))
160
+ end
161
+ script_log_file = File.open(script_log_file_location, 'w')
162
+ else
163
+ script_log_file = File.open(script_log_file_location, 'a')
164
+ end
165
+ yield(script_log_file)
166
+ ensure
167
+ script_log_file.close unless script_log_file.nil?
168
+ end
169
+
170
+ private
171
+ def script_absolute_path(script)
172
+ File.join(@deployment_archive_dir, script.location)
173
+ end
174
+
175
+ private
176
+ def parse_app_spec
177
+ app_spec_location = File.join(@deployment_archive_dir, @app_spec_path)
178
+ log(:debug, "Checking for app spec in #{app_spec_location}")
179
+ @app_spec = ApplicationSpecification::ApplicationSpecification.parse(File.read(app_spec_location))
180
+ end
181
+
182
+ private
183
+ def select_correct_deployment_root_dir(current_deployment_root_dir, last_successful_deployment_root_dir)
184
+ @deployment_root_dir = current_deployment_root_dir
185
+ hook_deployment_mapping = mapping_between_hooks_and_deployments
186
+ if(hook_deployment_mapping[@lifecycle_event] == LAST_SUCCESSFUL_DEPLOYMENT && !File.exist?(File.join(@deployment_root_dir, 'deployment-archive')))
187
+ @deployment_root_dir = last_successful_deployment_root_dir
188
+ end
189
+ end
190
+
191
+ private
192
+ def mapping_between_hooks_and_deployments
193
+ {"BeforeELBRemove"=>LAST_SUCCESSFUL_DEPLOYMENT,
194
+ "AfterELBRemove"=>LAST_SUCCESSFUL_DEPLOYMENT,
195
+ "ApplicationStop"=>LAST_SUCCESSFUL_DEPLOYMENT,
196
+ "BeforeInstall"=>CURRENT,
197
+ "AfterInstall"=>CURRENT,
198
+ "ApplicationStart"=>CURRENT,
199
+ "BeforeELBAdd"=>CURRENT,
200
+ "AfterELBAdd"=>CURRENT,
201
+ "ValidateService"=>CURRENT}
202
+ end
203
+
204
+ private
205
+ def description
206
+ self.class.to_s
207
+ end
208
+
209
+ private
210
+ def log(severity, message)
211
+ raise ArgumentError, "Unknown severity #{severity.inspect}" unless InstanceAgent::Log::SEVERITIES.include?(severity.to_s)
212
+ InstanceAgent::Log.send(severity.to_sym, "#{description}: #{message}")
213
+ end
214
+
215
+ private
216
+ def log_script(message, script_log_file)
217
+ @hook_logging_mutex.synchronize do
218
+ @script_log.append_to_log(message)
219
+ script_log_file.write(Time.now.to_s[0..-7] + ' ' + message)
220
+ script_log_file.flush
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end