aws-codedeploy-agent 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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