kitchen-ec2 3.3.0 → 3.7.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1132f5a8bcb58e6a8c4d38ad342ac2e38afeabbd51abf63b6980a6b4c9474339
4
- data.tar.gz: 876fb813038936051338a3be5682fc6633b420aa6faa201b3156879bc9f29430
3
+ metadata.gz: effb6d0cc14377657291cd970e2a6d50680d91df937b6799f0a942a2fba59994
4
+ data.tar.gz: a2a0b678c22cf3da32be2ee90cb542cc2c2dff015fb3563ea7783b85ddd57649
5
5
  SHA512:
6
- metadata.gz: ae1058987c9efd9e8b7e0f2e0fa152b0a7cefb297d82fb6ab50cc77e1ee84f59e7b9a7fb54b391e9a3b35ae3ee7f3ef704ab6edd7e8fb908c1abe90870cc2c3d
7
- data.tar.gz: 5fd929e0e1752d3b7cd1ff5b1e83383be321c95569bc00fee8ab9ad19539e815a3a1d9f692c516550cf5d4da5f667e500bc2ea8d64704a0fc337152fe34744df
6
+ metadata.gz: 2761f62c4647000ee4b12ac11a0080f49bb5492c296e08a22c598d1bfb0a0ea993b7d0409c04003d7819d7f287dd9fd65b611f2135c8edf69a5e36421549ea2c
7
+ data.tar.gz: 96b9d6c0062d85baafa9febca4fe0567f2659dc505c3e89da85caa3b6f82590f5a981d8df2907ee1f15709374e1e31f833f4cbb940a87b83283086e11084adab
@@ -49,14 +49,24 @@ module Kitchen
49
49
  ::Aws.config.update(retry_limit: retry_limit) unless retry_limit.nil?
50
50
  end
51
51
 
52
+ # create a new AWS EC2 instance
53
+ # @param options [Hash] has of instance options
54
+ # @see https://docs.aws.amazon.com/sdkforruby/api/Aws/EC2/Resource.html#create_instances-instance_method
55
+ # @return [Aws::EC2::Instance]
52
56
  def create_instance(options)
53
57
  resource.create_instances(options).first
54
58
  end
55
59
 
60
+ # get an instance object given an id
61
+ # @param id [String] aws instance id
62
+ # @return [Aws::EC2::Instance]
56
63
  def get_instance(id)
57
64
  resource.instance(id)
58
65
  end
59
66
 
67
+ # get an instance object given a spot request ID
68
+ # @param request_id [String] aws spot instance id
69
+ # @return [Aws::EC2::Instance]
60
70
  def get_instance_from_spot_request(request_id)
61
71
  resource.instances(
62
72
  filters: [{
@@ -39,8 +39,10 @@ module Kitchen
39
39
  @logger = logger
40
40
  end
41
41
 
42
- # Transform the provided config into the hash to send to AWS. Some fields
42
+ # Transform the provided kitchen config into the hash we'll use to create the aws instance
43
43
  # can be passed in null, others need to be ommitted if they are null
44
+ # Some fields can be passed in null, others need to be ommitted if they are null
45
+ # @return [Hash]
44
46
  def ec2_instance_data # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
45
47
  # Support for looking up security group id and subnet id using tags.
46
48
  vpc_id = nil
@@ -56,8 +58,10 @@ module Kitchen
56
58
  ).subnets
57
59
  raise "The subnet tagged '#{config[:subnet_filter][:tag]}:#{config[:subnet_filter][:value]}' does not exist!" unless subnets.any?
58
60
 
59
- vpc_id = subnets[0].vpc_id
60
- config[:subnet_id] = subnets[0].subnet_id
61
+ # => Select the least-populated subnet if we have multiple matches
62
+ subnet = subnets.sort_by { |s| s[:available_ip_address_count] }.last
63
+ vpc_id = subnet.vpc_id
64
+ config[:subnet_id] = subnet.subnet_id
61
65
  end
62
66
 
63
67
  if config[:security_group_ids].nil? && config[:security_group_filter]
@@ -111,8 +115,22 @@ module Kitchen
111
115
  key_name: config[:aws_ssh_key_id],
112
116
  subnet_id: config[:subnet_id],
113
117
  private_ip_address: config[:private_ip_address],
118
+ min_count: 1,
119
+ max_count: 1,
114
120
  }
115
121
 
122
+ if config[:tags] && !config[:tags].empty?
123
+ tags = config[:tags].map do |k, v|
124
+ # we convert the value to a string because
125
+ # nils should be passed as an empty String
126
+ # and Integers need to be represented as Strings
127
+ { key: k, value: v.to_s }
128
+ end
129
+ instance_tag_spec = { resource_type: "instance", tags: tags }
130
+ volume_tag_spec = { resource_type: "volume", tags: tags }
131
+ i[:tag_specifications] = [instance_tag_spec, volume_tag_spec]
132
+ end
133
+
116
134
  availability_zone = config[:availability_zone]
117
135
  if availability_zone
118
136
  if availability_zone =~ /^[a-z]$/i
@@ -23,6 +23,8 @@ module Kitchen
23
23
  class Amazon < StandardPlatform
24
24
  StandardPlatform.platforms["amazon"] = self
25
25
 
26
+ # default username for this platform's ami
27
+ # @return [String]
26
28
  def username
27
29
  "ec2-user"
28
30
  end
@@ -23,6 +23,8 @@ module Kitchen
23
23
  class Amazon2 < StandardPlatform
24
24
  StandardPlatform.platforms["amazon2"] = self
25
25
 
26
+ # default username for this platform's ami
27
+ # @return [String]
26
28
  def username
27
29
  "ec2-user"
28
30
  end
@@ -23,6 +23,16 @@ module Kitchen
23
23
  class Centos < StandardPlatform
24
24
  StandardPlatform.platforms["centos"] = self
25
25
 
26
+ CENTOS_OWNER_ID = "125523088429".freeze
27
+ PRODUCT_CODES = {
28
+ "6" => "6x5jmcajty9edm3f211pqjfn2",
29
+ "7" => "aw0evgkw8e5c1q413zgy5pjce",
30
+ # It appears that v8 is not published to the
31
+ # AWS marketplace and hence does not have a product code
32
+ }.freeze
33
+
34
+ # default username for this platform's ami
35
+ # @return [String]
26
36
  def username
27
37
  # Centos 6.x images use root as the username (but the "centos 6"
28
38
  # updateable image uses "centos")
@@ -32,10 +42,25 @@ module Kitchen
32
42
  end
33
43
 
34
44
  def image_search
45
+ # Version 8+ are published directly, not to the AWS marketplace. Use OWNER ID.
35
46
  search = {
36
- "owner-alias" => "aws-marketplace",
37
- "name" => ["CentOS Linux #{version}*", "CentOS-#{version}*-GA-*"],
47
+ "owner-id" => CENTOS_OWNER_ID,
48
+ "name" => ["CentOS #{version}*", "CentOS-#{version}*-GA-*"],
38
49
  }
50
+
51
+ if version && version.split(".").first.to_i < 8
52
+ # Versions <8 are published to the AWS marketplace and use a different naming convention
53
+ search = {
54
+ "owner-alias" => "aws-marketplace",
55
+ "name" => ["CentOS Linux #{version}*", "CentOS-#{version}*-GA-*"],
56
+ }
57
+ # For versions published to aws-marketplace, additionally filter on product code to
58
+ # avoid non-official AMIs. Can't use CentOS owner ID here, as the owner ID is that of aws marketplace.
59
+ # https://github.com/test-kitchen/kitchen-ec2/issues/456
60
+ PRODUCT_CODES.keys.each do |major_version|
61
+ search["product-code"] = PRODUCT_CODES[major_version] if version.start_with?(major_version)
62
+ end
63
+ end
39
64
  search["architecture"] = architecture if architecture
40
65
  search
41
66
  end
@@ -26,14 +26,17 @@ module Kitchen
26
26
  # 10/11 are listed last since we default to the first item in the hash
27
27
  # and 10/11 are not released yet. When they're released move them up
28
28
  DEBIAN_CODENAMES = {
29
+ 10 => "buster",
29
30
  9 => "stretch",
30
31
  8 => "jessie",
31
32
  7 => "wheezy",
32
33
  6 => "squeeze",
33
34
  11 => "bullseye",
34
- 10 => "buster",
35
+ 12 => "bookworm",
35
36
  }.freeze
36
37
 
38
+ # default username for this platform's ami
39
+ # @return [String]
37
40
  def username
38
41
  "admin"
39
42
  end
@@ -23,6 +23,8 @@ module Kitchen
23
23
  class Fedora < StandardPlatform
24
24
  StandardPlatform.platforms["fedora"] = self
25
25
 
26
+ # default username for this platform's ami
27
+ # @return [String]
26
28
  def username
27
29
  "fedora"
28
30
  end
@@ -23,6 +23,8 @@ module Kitchen
23
23
  class Freebsd < StandardPlatform
24
24
  StandardPlatform.platforms["freebsd"] = self
25
25
 
26
+ # default username for this platform's ami
27
+ # @return [String]
26
28
  def username
27
29
  "ec2-user"
28
30
  end
@@ -29,6 +29,8 @@ module Kitchen
29
29
  super(driver, "rhel", version, architecture)
30
30
  end
31
31
 
32
+ # default username for this platform's ami
33
+ # @return [String]
32
34
  def username
33
35
  (version && version.to_f < 6.4) ? "root" : "ec2-user"
34
36
  end
@@ -23,6 +23,8 @@ module Kitchen
23
23
  class Ubuntu < StandardPlatform
24
24
  StandardPlatform.platforms["ubuntu"] = self
25
25
 
26
+ # default username for this platform's ami
27
+ # @return [String]
26
28
  def username
27
29
  "ubuntu"
28
30
  end
@@ -23,6 +23,8 @@ module Kitchen
23
23
  class Windows < StandardPlatform
24
24
  StandardPlatform.platforms["windows"] = self
25
25
 
26
+ # default username for this platform's ami
27
+ # @return [String]
26
28
  def username
27
29
  "administrator"
28
30
  end
@@ -227,7 +227,7 @@ module Kitchen
227
227
 
228
228
  if config[:spot_price]
229
229
  # Spot instance when a price is set
230
- server = with_request_limit_backoff(state) { submit_spots(state) }
230
+ server = with_request_limit_backoff(state) { submit_spots }
231
231
  else
232
232
  # On-demand instance
233
233
  server = with_request_limit_backoff(state) { submit_server }
@@ -238,32 +238,16 @@ module Kitchen
238
238
  server.wait_until_exists(before_attempt: logging_proc)
239
239
  end
240
240
 
241
+ state[:server_id] = server.id
242
+ info("EC2 instance <#{state[:server_id]}> created.")
243
+
241
244
  # See https://github.com/aws/aws-sdk-ruby/issues/859
242
- # Tagging can fail with a NotFound error even though we waited until the server exists
243
- # Waiting can also fail, so we have to also retry on that. If it means we re-tag the
244
- # instance, so be it.
245
- # Tagging an instance is possible before volumes are attached. Tagging the volumes after
246
- # instance creation is consistent.
245
+ # Waiting can fail, so we have to retry on that.
247
246
  Retryable.retryable(
248
247
  tries: 10,
249
248
  sleep: lambda { |n| [2**n, 30].min },
250
249
  on: ::Aws::EC2::Errors::InvalidInstanceIDNotFound
251
250
  ) do |r, _|
252
- info("Attempting to tag the instance, #{r} retries")
253
- tag_server(server)
254
-
255
- # Get information about the AMI (image) used to create the image.
256
- image_data = ec2.client.describe_images({ image_ids: [server.image_id] })[0][0]
257
-
258
- state[:server_id] = server.id
259
- info("EC2 instance <#{state[:server_id]}> created.")
260
-
261
- # instance-store backed images do not have attached volumes, so only
262
- # wait for the volumes to be ready if the instance EBS-backed.
263
- if image_data.root_device_type == "ebs"
264
- wait_until_volumes_ready(server, state)
265
- tag_volumes(server)
266
- end
267
251
  wait_until_ready(server, state)
268
252
  end
269
253
 
@@ -291,14 +275,11 @@ module Kitchen
291
275
  server = ec2.get_instance(state[:server_id])
292
276
  unless server.nil?
293
277
  instance.transport.connection(state).close
294
- server.terminate
295
- end
296
- if state[:spot_request_id]
297
- debug("Deleting spot request <#{state[:server_id]}>")
298
- ec2.client.cancel_spot_instance_requests(
299
- spot_instance_request_ids: [state[:spot_request_id]]
300
- )
301
- state.delete(:spot_request_id)
278
+ begin
279
+ server.terminate
280
+ rescue ::Aws::EC2::Errors::InvalidInstanceIDNotFound => e
281
+ warn("Received #{e}, instance was probably already destroyed. Ignoring")
282
+ end
302
283
  end
303
284
  # If we are going to clean up an automatic security group, we need
304
285
  # to wait for the instance to shut down. This slightly breaks the
@@ -405,15 +386,14 @@ module Kitchen
405
386
  @instance_generator = Aws::InstanceGenerator.new(config, ec2, instance.logger)
406
387
  end
407
388
 
408
- # Fog AWS helper for creating the instance
389
+ # AWS helper for creating the instance
409
390
  def submit_server
410
391
  instance_data = instance_generator.ec2_instance_data
411
392
  debug("Creating EC2 instance in region #{config[:region]} with properties:")
412
393
  instance_data.each do |key, value|
413
394
  debug("- #{key} = #{value.inspect}")
414
395
  end
415
- instance_data[:min_count] = 1
416
- instance_data[:max_count] = 1
396
+
417
397
  ec2.create_instance(instance_data)
418
398
  end
419
399
 
@@ -441,10 +421,34 @@ module Kitchen
441
421
  configs
442
422
  end
443
423
 
444
- def submit_spots(state)
424
+ def submit_spots
445
425
  configs = [config]
446
426
  expanded = []
447
- keys = %i{instance_type subnet_id}
427
+ keys = %i{instance_type}
428
+
429
+ unless config[:subnet_filter]
430
+ # => Use explicitly specified subnets
431
+ keys << :subnet_id
432
+ else
433
+ # => Enable cascading through matching subnets
434
+ client = ::Aws::EC2::Client.new(region: config[:region])
435
+ subnets = client.describe_subnets(
436
+ filters: [
437
+ {
438
+ name: "tag:#{config[:subnet_filter][:tag]}",
439
+ values: [config[:subnet_filter][:value]],
440
+ },
441
+ ]
442
+ ).subnets
443
+ raise "A subnet matching '#{config[:subnet_filter][:tag]}:#{config[:subnet_filter][:value]}' does not exist!" unless subnets.any?
444
+
445
+ configs = subnets.map do |subnet|
446
+ new_config = config.clone
447
+ new_config[:subnet_id] = subnet.subnet_id
448
+ new_config[:subnet_filter] = nil
449
+ new_config
450
+ end
451
+ end
448
452
 
449
453
  keys.each do |key|
450
454
  configs.each do |conf|
@@ -458,7 +462,7 @@ module Kitchen
458
462
  configs.each do |conf|
459
463
  begin
460
464
  @config = conf
461
- return submit_spot(state)
465
+ return submit_spot
462
466
  rescue => e
463
467
  errs.append(e)
464
468
  end
@@ -466,29 +470,10 @@ module Kitchen
466
470
  raise ["Could not create a spot instance:", errs].flatten.join("\n")
467
471
  end
468
472
 
469
- def submit_spot(state)
473
+ def submit_spot
470
474
  debug("Creating EC2 Spot Instance..")
475
+ instance_data = instance_generator.ec2_instance_data
471
476
 
472
- spot_request_id = create_spot_request
473
- # deleting the instance cancels the request, but deleting the request
474
- # does not affect the instance
475
- state[:spot_request_id] = spot_request_id
476
- ec2.client.wait_until(
477
- :spot_instance_request_fulfilled,
478
- spot_instance_request_ids: [spot_request_id]
479
- ) do |w|
480
- w.max_attempts = config[:spot_wait] / config[:retryable_sleep]
481
- w.delay = config[:retryable_sleep]
482
- w.before_attempt do |attempts|
483
- c = attempts * config[:retryable_sleep]
484
- t = config[:spot_wait]
485
- info "Waited #{c}/#{t}s for spot request <#{spot_request_id}> to become fulfilled."
486
- end
487
- end
488
- ec2.get_instance_from_spot_request(spot_request_id)
489
- end
490
-
491
- def create_spot_request
492
477
  request_duration = config[:spot_wait]
493
478
  config_spot_price = config[:spot_price].to_s
494
479
  if %w{ondemand on-demand}.include?(config_spot_price)
@@ -496,56 +481,36 @@ module Kitchen
496
481
  else
497
482
  spot_price = config_spot_price
498
483
  end
499
- request_data = {
500
- spot_price: spot_price,
501
- launch_specification: instance_generator.ec2_instance_data,
484
+ spot_options = {
485
+ spot_instance_type: "persistent", # Cannot use one-time with valid_until
502
486
  valid_until: Time.now + request_duration,
487
+ instance_interruption_behavior: "stop",
503
488
  }
504
489
  if config[:block_duration_minutes]
505
- request_data[:block_duration_minutes] = config[:block_duration_minutes]
490
+ spot_options[:block_duration_minutes] = config[:block_duration_minutes]
506
491
  end
507
-
508
- response = ec2.client.request_spot_instances(request_data)
509
- response[:spot_instance_requests][0][:spot_instance_request_id]
510
- end
511
-
512
- def tag_server(server)
513
- if config[:tags] && !config[:tags].empty?
514
- tags = config[:tags].map do |k, v|
515
- # we convert the value to a string because
516
- # nils should be passed as an empty String
517
- # and Integers need to be represented as Strings
518
- { key: k.to_s, value: v.to_s }
519
- end
520
- server.create_tags(tags: tags)
492
+ unless spot_price == "" # i.e. on-demand
493
+ spot_options[:max_price] = spot_price
521
494
  end
522
- end
523
495
 
524
- def tag_volumes(server)
525
- if config[:tags] && !config[:tags].empty?
526
- tags = config[:tags].map do |k, v|
527
- { key: k.to_s, value: v.to_s }
528
- end
529
- server.volumes.each do |volume|
530
- volume.create_tags(tags: tags)
531
- end
532
- end
533
- end
496
+ instance_data[:instance_market_options] = {
497
+ market_type: "spot",
498
+ spot_options: spot_options,
499
+ }
534
500
 
535
- # Compares the requested volume count vs what has actually been set to be
536
- # attached to the instance. The information requested through
537
- # ec2.client.described_volumes is updated before the instance volume
538
- # information.
539
- def wait_until_volumes_ready(server, state)
540
- wait_with_destroy(server, state, "volumes to be ready") do |aws_instance|
541
- described_volume_count = 0
542
- ready_volume_count = 0
543
- if aws_instance.exists?
544
- described_volume_count = ec2.client.describe_volumes(filters: [
545
- { name: "attachment.instance-id", values: ["#{state[:server_id]}"] }]).volumes.length
546
- aws_instance.volumes.each { ready_volume_count += 1 }
547
- end
548
- (described_volume_count > 0) && (described_volume_count == ready_volume_count)
501
+ # The preferred way to create a spot instance is via request_spot_instances()
502
+ # However, it does not allow for tagging to occur at creation time.
503
+ # create_instances() allows creation of tagged spot instances, but does
504
+ # not retry if the price could not be satisfied immediately.
505
+ Retryable.retryable(
506
+ tries: config[:spot_wait] / config[:retryable_sleep],
507
+ sleep: lambda { |_n| config[:retryable_sleep] },
508
+ on: ::Aws::EC2::Errors::SpotMaxPriceTooLow
509
+ ) do |retries|
510
+ c = retries * config[:retryable_sleep]
511
+ t = config[:spot_wait]
512
+ info "Waited #{c}/#{t}s for spot request to become fulfilled."
513
+ ec2.create_instance(instance_data)
549
514
  end
550
515
  end
551
516
 
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
4
  #
5
- # Copyright:: 2016-2020, Chef Software, Inc.
5
+ # Copyright:: Chef Software, Inc.
6
6
  # Copyright:: 2012-2018, Fletcher Nichol
7
7
  #
8
8
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,6 +22,6 @@ module Kitchen
22
22
  module Driver
23
23
 
24
24
  # Version string for EC2 Test Kitchen driver
25
- EC2_VERSION = "3.3.0".freeze
25
+ EC2_VERSION = "3.7.1".freeze
26
26
  end
27
27
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-ec2
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.0
4
+ version: 3.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fletcher Nichol
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-15 00:00:00.000000000 Z
11
+ date: 2020-07-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: test-kitchen
@@ -30,34 +30,6 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '3'
33
- - !ruby/object:Gem::Dependency
34
- name: excon
35
- requirement: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: '0'
40
- type: :runtime
41
- prerelease: false
42
- version_requirements: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: '0'
47
- - !ruby/object:Gem::Dependency
48
- name: multi_json
49
- requirement: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- version: '0'
54
- type: :runtime
55
- prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '0'
61
33
  - !ruby/object:Gem::Dependency
62
34
  name: aws-sdk-ec2
63
35
  requirement: !ruby/object:Gem::Requirement
@@ -134,20 +106,6 @@ dependencies:
134
106
  - - "~>"
135
107
  - !ruby/object:Gem::Version
136
108
  version: '0.6'
137
- - !ruby/object:Gem::Dependency
138
- name: simplecov
139
- requirement: !ruby/object:Gem::Requirement
140
- requirements:
141
- - - "~>"
142
- - !ruby/object:Gem::Version
143
- version: '0.7'
144
- type: :development
145
- prerelease: false
146
- version_requirements: !ruby/object:Gem::Requirement
147
- requirements:
148
- - - "~>"
149
- - !ruby/object:Gem::Version
150
- version: '0.7'
151
109
  - !ruby/object:Gem::Dependency
152
110
  name: yard
153
111
  requirement: !ruby/object:Gem::Requirement
@@ -168,14 +126,14 @@ dependencies:
168
126
  requirements:
169
127
  - - '='
170
128
  - !ruby/object:Gem::Version
171
- version: 0.14.0
129
+ version: 1.1.2
172
130
  type: :development
173
131
  prerelease: false
174
132
  version_requirements: !ruby/object:Gem::Requirement
175
133
  requirements:
176
134
  - - '='
177
135
  - !ruby/object:Gem::Version
178
- version: 0.14.0
136
+ version: 1.1.2
179
137
  - !ruby/object:Gem::Dependency
180
138
  name: climate_control
181
139
  requirement: !ruby/object:Gem::Requirement
@@ -224,14 +182,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
224
182
  requirements:
225
183
  - - ">="
226
184
  - !ruby/object:Gem::Version
227
- version: '2.3'
185
+ version: '2.4'
228
186
  required_rubygems_version: !ruby/object:Gem::Requirement
229
187
  requirements:
230
188
  - - ">="
231
189
  - !ruby/object:Gem::Version
232
190
  version: '0'
233
191
  requirements: []
234
- rubygems_version: 3.0.3
192
+ rubygems_version: 3.1.2
235
193
  signing_key:
236
194
  specification_version: 4
237
195
  summary: A Test Kitchen Driver for Amazon EC2