kitchen-ec2 3.3.0 → 3.7.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.
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