Skip to content

Commit aff5b11

Browse files
author
n.yovchev
committed
Merge branch 'master' into issue-182
2 parents d08f902 + baaa837 commit aff5b11

33 files changed

+3248
-228
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ codedeploy-local.*.log
1010
deployment/
1111
.idea/
1212
.DS_STORE
13+
*.iml

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ group :test do
1616
gem 'fakefs', :require => 'fakefs/safe'
1717
gem 'mocha'
1818
gem 'rspec'
19+
gem 'webmock', :require => 'webmock/rspec'
1920
gem 'shoulda'
2021
gem 'shoulda-matchers'
2122
gem 'shoulda-context'

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Please do the build steps mentioned above before running the integration test.
2828
The integration test creates the following
2929
* An IAM role "codedeploy-agent-integ-test-deployment-role" if it doesn't exist
3030
* An IAM role "codedeploy-agent-integ-test-instance-role" if it doesn't exist
31+
* An IAM user "codedeploy-agent-integ-test-instance-user" if it doesn't exist. (Access key will be recreated.)
3132
* A CodeDeploy application
3233
* Startup the codedeploy agent on your host
3334
* A CodeDeploy deployment group with your host in it

bin/install

Lines changed: 171 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
# than 2.0. Testing on multiple Ruby versions is required for
66
# changes to this part of the code.
77
##################################################################
8-
require 'json'
9-
108
class Proxy
119
instance_methods.each do |m|
1210
undef_method m unless m =~ /(^__|^send$|^object_id$)/
@@ -42,6 +40,123 @@ end
4240

4341
@log.level = Logger::INFO
4442

43+
require 'net/http'
44+
45+
# This class is copied (almost directly) from lib/instance_metadata.rb
46+
# It is not loaded as the InstanceMetadata makes additional assumptions
47+
# about the runtime that cannot be satisfied at install time, hence the
48+
# trimmed copy.
49+
class IMDS
50+
IP_ADDRESS = '169.254.169.254'
51+
TOKEN_PATH = '/latest/api/token'
52+
BASE_PATH = '/latest/meta-data'
53+
IDENTITY_DOCUMENT_PATH = '/latest/dynamic/instance-identity/document'
54+
DOMAIN_PATH = '/latest/meta-data/services/domain'
55+
56+
def self.imds_supported?
57+
imds_v2? || imds_v1?
58+
end
59+
60+
def self.imds_v1?
61+
begin
62+
get_request(BASE_PATH) { |response|
63+
return response.kind_of? Net::HTTPSuccess
64+
}
65+
rescue
66+
false
67+
end
68+
end
69+
70+
def self.imds_v2?
71+
begin
72+
put_request(TOKEN_PATH) { |token_response|
73+
(token_response.kind_of? Net::HTTPSuccess) && get_request(BASE_PATH, token_response.body) { |response|
74+
return response.kind_of? Net::HTTPSuccess
75+
}
76+
}
77+
rescue
78+
false
79+
end
80+
end
81+
82+
def self.region
83+
begin
84+
identity_document()['region'].strip
85+
rescue
86+
nil
87+
end
88+
end
89+
90+
def self.domain
91+
begin
92+
get_instance_metadata(DOMAIN_PATH).strip
93+
rescue
94+
nil
95+
end
96+
end
97+
98+
def self.identity_document
99+
# JSON is lazy loaded to ensure we dont break older ruby runtimes
100+
require 'json'
101+
JSON.parse(get_instance_metadata(IDENTITY_DOCUMENT_PATH).strip)
102+
end
103+
104+
private
105+
def self.get_instance_metadata(path)
106+
begin
107+
token = put_request(TOKEN_PATH)
108+
get_request(path, token)
109+
rescue
110+
get_request(path)
111+
end
112+
end
113+
114+
115+
private
116+
def self.http_request(request)
117+
Net::HTTP.start(IP_ADDRESS, 80, :read_timeout => 10, :open_timeout => 10) do |http|
118+
response = http.request(request)
119+
if block_given?
120+
yield(response)
121+
elsif response.kind_of? Net::HTTPSuccess
122+
response.body
123+
else
124+
raise "HTTP error from metadata service: #{response.message}, code #{response.code}"
125+
end
126+
end
127+
end
128+
129+
def self.put_request(path, &block)
130+
request = Net::HTTP::Put.new(path)
131+
request['X-aws-ec2-metadata-token-ttl-seconds'] = '21600'
132+
http_request(request, &block)
133+
end
134+
135+
def self.get_request(path, token = nil, &block)
136+
request = Net::HTTP::Get.new(path)
137+
unless token.nil?
138+
request['X-aws-ec2-metadata-token'] = token
139+
end
140+
http_request(request, &block)
141+
end
142+
end
143+
144+
class S3Bucket
145+
# Split out as older versions of ruby dont like multi entry attr
146+
attr :domain
147+
attr :region
148+
attr :bucket
149+
def initialize(domain, region, bucket)
150+
@domain = domain
151+
@region = region
152+
@bucket = bucket
153+
end
154+
155+
def object_uri(object_key)
156+
URI.parse("https://#{@bucket}.s3.#{@region}.#{@domain}/#{object_key}")
157+
end
158+
end
159+
45160
begin
46161
require 'fileutils'
47162
require 'openssl'
@@ -151,6 +266,7 @@ EOF
151266
@sanity_check = true
152267
when '--help'
153268
usage
269+
exit(0)
154270
when '--re-execed'
155271
@reexeced = true
156272
when '--proxy'
@@ -187,13 +303,19 @@ EOF
187303
end
188304
end
189305

306+
parse_args()
307+
308+
# Be helpful when 'help' was used but not '--help'
309+
if @type == 'help'
310+
usage
311+
exit(0)
312+
end
313+
190314
if (Process.uid != 0)
191315
@log.error('Must run as root to install packages')
192316
exit(1)
193317
end
194318

195-
parse_args()
196-
197319
########## Force running as Ruby 2.x or fail here ##########
198320
ruby_interpreter_path = check_ruby_version_and_symlink
199321
force_ruby2x(ruby_interpreter_path)
@@ -206,16 +328,17 @@ EOF
206328
return exit_ok
207329
end
208330

209-
def get_ec2_metadata_region
210-
begin
211-
uri = URI.parse('http://169.254.169.254/latest/dynamic/instance-identity/document')
212-
document_string = uri.read(:read_timeout => 120)
213-
doc = JSON.parse(document_string.strip)
214-
return doc['region'].strip
215-
rescue
216-
@log.warn("Could not get region from EC2 metadata service at '#{uri.to_s}'")
217-
return nil
331+
def get_ec2_metadata_property(property)
332+
if IMDS.imds_supported?
333+
begin
334+
return IMDS.send(property)
335+
rescue => error
336+
@log.warn("Could not get #{property} from EC2 metadata service at '#{error.message}'")
337+
end
338+
else
339+
@log.warn("EC2 metadata service unavailable...")
218340
end
341+
return nil
219342
end
220343

221344
def get_region
@@ -224,27 +347,36 @@ EOF
224347
return region if region
225348

226349
@log.info('Checking EC2 metadata service for region information...')
227-
region = get_ec2_metadata_region
350+
region = get_ec2_metadata_property(:region)
228351
return region if region
229352

230353
@log.info('Using fail-safe default region: us-east-1')
231354
return 'us-east-1'
232355
end
233356

234-
def get_s3_uri(region, bucket, key)
235-
if (region == 'us-east-1')
236-
URI.parse("https://#{bucket}.s3.amazonaws.com/#{key}")
237-
elsif (region.split("-")[0] == 'cn')
238-
URI.parse("https://#{bucket}.s3.#{region}.amazonaws.com.cn/#{key}")
239-
else
240-
URI.parse("https://#{bucket}.s3.#{region}.amazonaws.com/#{key}")
357+
def get_domain(fallback_region = nil)
358+
@log.info('Checking AWS_DOMAIN environment variable for domain information...')
359+
domain = ENV['AWS_DOMAIN']
360+
return domain if domain
361+
362+
@log.info('Checking EC2 metadata service for domain information...')
363+
domain = get_ec2_metadata_property(:domain)
364+
return domain if domain
365+
366+
domain = 'amazonaws.com'
367+
if !fallback_region.nil? && fallback_region.split("-")[0] == 'cn'
368+
domain = 'amazonaws.com.cn'
241369
end
370+
371+
@log.info("Using fail-safe default domain: #{domain}")
372+
return domain
242373
end
243374

244-
def get_package_from_s3(region, bucket, key, package_file)
245-
@log.info("Downloading package from bucket #{bucket} and key #{key}...")
375+
def get_package_from_s3(s3_bucket, key, package_file)
376+
@log.info("Downloading package from bucket #{s3_bucket.bucket} and key #{key}...")
246377

247-
uri = get_s3_uri(region, bucket, key)
378+
uri = s3_bucket.object_uri(key)
379+
@log.info("Endpoint: #{uri}")
248380

249381
# stream package file to disk
250382
retries ||= 0
@@ -266,10 +398,11 @@ EOF
266398
end
267399
end
268400

269-
def get_version_file_from_s3(region, bucket, key)
270-
@log.info("Downloading version file from bucket #{bucket} and key #{key}...")
401+
def get_version_file_from_s3(s3_bucket, key)
402+
@log.info("Downloading version file from bucket #{s3_bucket.bucket} and key #{key}...")
271403

272-
uri = get_s3_uri(region, bucket, key)
404+
uri = s3_bucket.object_uri(key)
405+
@log.info("Endpoint: #{uri}")
273406

274407
begin
275408
require 'json'
@@ -282,13 +415,13 @@ end
282415
end
283416
end
284417

285-
def install_from_s3(region, bucket, package_key, install_cmd)
418+
def install_from_s3(s3_bucket, package_key, install_cmd)
286419
package_base_name = File.basename(package_key)
287420
package_extension = File.extname(package_base_name)
288421
package_name = File.basename(package_base_name, package_extension)
289422
package_file = Tempfile.new(["#{package_name}.tmp-", package_extension]) # unique file with 0600 permissions
290423

291-
get_package_from_s3(region, bucket, package_key, package_file)
424+
get_package_from_s3(s3_bucket, package_key, package_file)
292425
package_file.close
293426

294427
install_cmd << package_file.path
@@ -315,10 +448,10 @@ end
315448
end
316449
end
317450

318-
def get_target_version(target_version, type, region, bucket)
451+
def get_target_version(target_version, type, s3_bucket)
319452
if target_version.nil?
320453
version_file_key = 'latest/LATEST_VERSION'
321-
version_data = get_version_file_from_s3(region, bucket, version_file_key)
454+
version_data = get_version_file_from_s3(s3_bucket, version_file_key)
322455
if version_data.include? type
323456
return version_data[type]
324457
else
@@ -365,14 +498,14 @@ end
365498
end
366499
end
367500

368-
region = get_region
501+
region = get_region()
502+
domain = get_domain(region)
369503
bucket = "aws-codedeploy-#{region}"
504+
s3_bucket = S3Bucket.new(domain, region, bucket)
370505

371-
target_version = get_target_version(@target_version_arg, @type, region, bucket)
506+
target_version = get_target_version(@target_version_arg, @type, s3_bucket)
372507

373508
case @type
374-
when 'help'
375-
usage
376509
when 'rpm'
377510
running_version = `rpm -q codedeploy-agent`
378511
running_version.strip!
@@ -381,7 +514,7 @@ end
381514
else
382515
#use -y to answer yes to confirmation prompts
383516
install_cmd = ['/usr/bin/yum', '-y', 'localinstall']
384-
install_from_s3(region, bucket, target_version, install_cmd)
517+
install_from_s3(s3_bucket, target_version, install_cmd)
385518
do_sanity_check('/sbin/service')
386519
end
387520
when 'deb'
@@ -400,13 +533,13 @@ end
400533
#use -n for non-interactive mode
401534
#use -o to not overwrite config files unless they have not been changed
402535
install_cmd = ['/usr/bin/gdebi', '-n', '-o', 'Dpkg::Options::=--force-confdef', '-o', 'Dkpg::Options::=--force-confold']
403-
install_from_s3(region, bucket, target_version, install_cmd)
536+
install_from_s3(s3_bucket, target_version, install_cmd)
404537
do_sanity_check('/usr/sbin/service')
405538
end
406539
when 'zypper'
407540
#use -n for non-interactive mode
408541
install_cmd = ['/usr/bin/zypper', 'install', '-n']
409-
install_from_s3(region, bucket, target_version, install_cmd)
542+
install_from_s3(s3_bucket, target_version, install_cmd)
410543
else
411544
@log.error("Unsupported package type '#{@type}'")
412545
exit(1)

0 commit comments

Comments
 (0)