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