kitchen-cloudformation 1.3.0 → 1.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +34 -3
- data/kitchen-cloudformation.gemspec +2 -2
- data/lib/kitchen/driver/aws/cf_client.rb +21 -0
- data/lib/kitchen/driver/aws/stack_generator.rb +1 -1
- data/lib/kitchen/driver/cloudformation.rb +106 -11
- data/lib/kitchen/driver/cloudformation_version.rb +1 -1
- data/lib/kitchen/provisioner/cloudformation.rb +47 -0
- metadata +15 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 96f4545854d99212bb47aa812fbfedf485b674855f19ef36f5eff82548ca6a63
|
4
|
+
data.tar.gz: d3a7035c27afc23f3527d89d1ba0efcce65ceb265592e889dbcd30f1df60494c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 923ed16c853ad13e22ab8b0eb39d66106b6cf81aee6646a4673a138d8bcdc5e55efb0f83e50e774900ec936924087594de98d838d899bb2808bf34d1e24f0d28
|
7
|
+
data.tar.gz: 5a274acd9f323d95b4bcc9744fd741bab91768dea2e831cb39755fd8d21973cc0dc3765e41a29c9982528c32a50e417d92cca622cc430d66f6b87bf72f85bd77
|
data/README.md
CHANGED
@@ -12,7 +12,7 @@ If you wish to use servers specified as a hostname in the converge step then use
|
|
12
12
|
So you can deploy and test say a Mongodb High Availability cluster by using cloud formation to create the servers
|
13
13
|
and then converge each of the servers in the cluster and run tests.
|
14
14
|
|
15
|
-
This can be used with [kitchen-verifier-awspec](https://github.com/neillturner/kitchen-verifier-awspec) to do verification of AWS infrastructure.
|
15
|
+
This can be used with [kitchen-inspec](https://github.com/chef/kitchen-inspec) or [kitchen-verifier-awspec](https://github.com/neillturner/kitchen-verifier-awspec) to do verification of AWS infrastructure.
|
16
16
|
|
17
17
|
## Requirements
|
18
18
|
|
@@ -32,6 +32,9 @@ shared_credentials_profile| nil|Specify Credentials Using a Profile Name
|
|
32
32
|
key | default value | Notes
|
33
33
|
----|---------------|--------
|
34
34
|
capabilities||Array of capabilities that must be specified before creating or updating certain stacks accepts CAPABILITY_IAM, CAPABILITY_NAMED_IAM
|
35
|
+
change_set_name ||Name of the Cloud Formation Change Set to create and then execute at converge time
|
36
|
+
change_set_template_file||File containing the Cloudformation template to use to create the change set
|
37
|
+
change_set_type | UPDATE |Cloud Formation Change Set can be CREATE or UPDATE
|
35
38
|
disable_rollback||If the template gets an error don't rollback changes. true/false. default false.
|
36
39
|
notification_arns| [] |The Simple Notification Service (SNS) topic ARNs to publish stack related events. Array of Strings.
|
37
40
|
on_failure||Determines what action will be taken if stack creation fails. accepts DO_NOTHING, ROLLBACK, DELETE. You can specify either on_failure or disable_rollback, but not both.
|
@@ -80,6 +83,22 @@ through CI we no longer recommend storing the AWS credentials in the
|
|
80
83
|
`.kitchen.yml` file. Instead, specify them as environment variables or in the
|
81
84
|
`~/.aws/credentials` file.
|
82
85
|
|
86
|
+
## Change Sets
|
87
|
+
|
88
|
+
Normally kitchen-cloudformation creates the stack from the template file when the kitchen create command if run.
|
89
|
+
|
90
|
+
If you specify a change_set_name and a change_set_template_file
|
91
|
+
|
92
|
+
```yaml
|
93
|
+
template_file: TestSecurityGroup.template
|
94
|
+
change_set_name: mystack-cs
|
95
|
+
change_set_template_file: TestSecurityGroupCs.template
|
96
|
+
```
|
97
|
+
|
98
|
+
then it will also create the change set from the change_set_template_file.
|
99
|
+
|
100
|
+
Then when the kitchen converge is run it will apply the change-set so it can be tested.
|
101
|
+
|
83
102
|
|
84
103
|
## SSL Certificate File Issues
|
85
104
|
|
@@ -94,6 +113,16 @@ A file ca-bundle.crt is supplied inside this gem for this purpose so you can set
|
|
94
113
|
|
95
114
|
## Example
|
96
115
|
|
116
|
+
See example at https://github.com/neillturner/cloudformation_repo
|
117
|
+
|
118
|
+
kitchen create default-test -l debug
|
119
|
+
|
120
|
+
Create the stack if it does not exist and creates a change set if one is specified.
|
121
|
+
|
122
|
+
kitchen converge default-test -l debug
|
123
|
+
|
124
|
+
Executes the change set if one has been created
|
125
|
+
|
97
126
|
The following could be used in a `.kitchen.yml` or in a `.kitchen.local.yml`
|
98
127
|
to override default configuration.
|
99
128
|
|
@@ -105,12 +134,14 @@ driver:
|
|
105
134
|
template_file: /test/example.template
|
106
135
|
parameters:
|
107
136
|
base_package: wget
|
137
|
+
change_set_name: mystack-cs
|
138
|
+
change_set_template_file: TestSecurityGroupCs.template
|
108
139
|
|
109
140
|
provisioner:
|
110
|
-
name:
|
141
|
+
name: Cloudformation
|
111
142
|
|
112
143
|
platforms:
|
113
|
-
- name:
|
144
|
+
- name: test
|
114
145
|
driver: Cloudformation
|
115
146
|
|
116
147
|
suites:
|
@@ -16,9 +16,9 @@ Gem::Specification.new do |gem|
|
|
16
16
|
gem.executables = []
|
17
17
|
gem.require_paths = ['lib']
|
18
18
|
gem.required_ruby_version = '>= 2.2.2'
|
19
|
-
gem.add_dependency '
|
19
|
+
gem.add_dependency 'aws-sdk', '~> 3'
|
20
20
|
gem.add_dependency 'excon'
|
21
21
|
gem.add_dependency 'multi_json'
|
22
|
-
gem.add_dependency 'aws-sdk', '~> 2'
|
23
22
|
gem.add_dependency 'retryable', '~> 2.0'
|
23
|
+
gem.add_dependency 'test-kitchen', '>= 1.4.1'
|
24
24
|
end
|
@@ -94,10 +94,31 @@ module Kitchen
|
|
94
94
|
resource.create_stack(options)
|
95
95
|
end
|
96
96
|
|
97
|
+
def update_stack(options)
|
98
|
+
resource.update_stack(options)
|
99
|
+
end
|
100
|
+
|
101
|
+
def create_change_set(options)
|
102
|
+
client.create_change_set(options)
|
103
|
+
end
|
104
|
+
|
105
|
+
def execute_change_set(options)
|
106
|
+
client.execute_change_set(options)
|
107
|
+
end
|
108
|
+
|
97
109
|
def get_stack(stack_name)
|
98
110
|
resource.stack(stack_name)
|
99
111
|
end
|
100
112
|
|
113
|
+
# rubocop:disable Lint/RescueWithoutErrorClass
|
114
|
+
def describe_change_set(stack_name, change_set_name)
|
115
|
+
client.describe_change_set(change_set_name: change_set_name,
|
116
|
+
stack_name: stack_name)
|
117
|
+
rescue
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
# rubocop:enable Lint/RescueWithoutErrorClass
|
121
|
+
|
101
122
|
def get_stack_events(stack_name)
|
102
123
|
client.describe_stack_events(stack_name: stack_name)
|
103
124
|
end
|
@@ -34,7 +34,7 @@ module Kitchen
|
|
34
34
|
# can be passed in null, others need to be ommitted if they are null
|
35
35
|
def cf_stack_data
|
36
36
|
s = { stack_name: config[:stack_name] }
|
37
|
-
s[:template_url] = config[:template_url] if config[:template_file]
|
37
|
+
s[:template_url] = config[:template_url] if config[:template_file].nil?
|
38
38
|
if config[:template_file]
|
39
39
|
s[:template_body] = File.open(config[:template_file], 'rb') { |file| file.read }
|
40
40
|
end
|
@@ -37,12 +37,12 @@ module Kitchen
|
|
37
37
|
default_config :ssl_verify_peer, true
|
38
38
|
default_config :stack_name, nil
|
39
39
|
default_config :template_file, nil
|
40
|
+
|
40
41
|
default_config :capabilities, nil
|
41
42
|
default_config :parameters, {}
|
42
43
|
default_config :disable_rollback, nil
|
43
44
|
default_config :timeout_in_minutes, 0
|
44
45
|
default_config :parameters, {}
|
45
|
-
|
46
46
|
default_config :ssh_key, nil
|
47
47
|
default_config :username, 'root'
|
48
48
|
default_config :hostname, nil
|
@@ -53,13 +53,16 @@ module Kitchen
|
|
53
53
|
default_config :stack_policy_body, nil
|
54
54
|
default_config :stack_policy_url, nil
|
55
55
|
default_config :tags, {}
|
56
|
+
default_config :change_set_name, nil
|
57
|
+
default_config :change_set_type, 'UPDATE'
|
58
|
+
default_config :change_set_template_file, nil
|
56
59
|
|
57
60
|
required_config :stack_name
|
58
61
|
|
59
62
|
# rubocop:disable Lint/RescueWithoutErrorClass
|
60
63
|
def create(state)
|
61
64
|
copy_deprecated_configs(state)
|
62
|
-
return if state[:stack_name]
|
65
|
+
# return if state[:stack_name]
|
63
66
|
|
64
67
|
info(Kitchen::Util.outdent!(<<-TEXT))
|
65
68
|
Creating CloudFormation Stack <#{config[:stack_name]}>...
|
@@ -68,33 +71,83 @@ module Kitchen
|
|
68
71
|
should be minimal, but neither Test Kitchen nor its maintainers
|
69
72
|
are responsible for your incurred costs.
|
70
73
|
TEXT
|
74
|
+
unless stack_exists
|
75
|
+
begin
|
76
|
+
stack = create_stack
|
77
|
+
rescue
|
78
|
+
error("CloudFormation #{$ERROR_INFO}.") # e.message
|
79
|
+
return
|
80
|
+
end
|
81
|
+
state[:stack_name] = stack.stack_name unless state[:stack_name]
|
82
|
+
info("Stack <#{state[:stack_name]}> requested.")
|
83
|
+
# tag_stack(stack)
|
84
|
+
|
85
|
+
s = cf.get_stack(state[:stack_name])
|
86
|
+
while s.stack_status == 'CREATE_IN_PROGRESS'
|
87
|
+
debug_stack_events(state[:stack_name])
|
88
|
+
info("CloudFormation waiting for stack <#{state[:stack_name]}> to be created.....")
|
89
|
+
sleep(30)
|
90
|
+
s = cf.get_stack(state[:stack_name])
|
91
|
+
end
|
92
|
+
display_stack_events(state[:stack_name])
|
93
|
+
if s.stack_status == 'CREATE_COMPLETE'
|
94
|
+
outputs = Hash[*s.outputs.map do |o|
|
95
|
+
[o[:output_key], o[:output_value]]
|
96
|
+
end.flatten]
|
97
|
+
state[:hostname] = config[:hostname].gsub(/\${([^}]+)}/) { outputs[Regexp.last_match(1)] || '' } if config[:hostname]
|
98
|
+
info("CloudFormation stack <#{state[:stack_name]}> created.")
|
99
|
+
else
|
100
|
+
error("CloudFormation stack <#{stack.stack_name}> failed to create....attempting to delete")
|
101
|
+
destroy(state)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
return if change_set_exists
|
105
|
+
begin
|
106
|
+
create_change_set
|
107
|
+
rescue
|
108
|
+
error("CloudFormation #{$ERROR_INFO}.") # e.message
|
109
|
+
return
|
110
|
+
end
|
111
|
+
state[:stack_name] = config[:stack_name] unless state[:stack_name]
|
112
|
+
end
|
113
|
+
|
114
|
+
def update(state)
|
115
|
+
info("Stack <#{state[:stack_name]}> Execute Change Set requested to update stack.")
|
116
|
+
stack = cf.get_stack(state[:stack_name])
|
117
|
+
if stack.nil?
|
118
|
+
info("CloudFormation stack <#{state[:stack_name]}> doesn't exist.")
|
119
|
+
return
|
120
|
+
end
|
121
|
+
unless change_set_exists
|
122
|
+
info("CloudFormation change set <#{config[:change_set_name]}> doesn't exist for stack <#{state[:stack_name]}>.")
|
123
|
+
return
|
124
|
+
end
|
71
125
|
begin
|
72
|
-
stack =
|
126
|
+
stack = execute_change_set
|
73
127
|
rescue
|
74
128
|
error("CloudFormation #{$ERROR_INFO}.") # e.message
|
75
129
|
return
|
76
130
|
end
|
77
|
-
state[:stack_name] = stack.stack_name
|
131
|
+
state[:stack_name] = stack.stack_name unless state[:stack_name]
|
78
132
|
info("Stack <#{state[:stack_name]}> requested.")
|
79
133
|
# tag_stack(stack)
|
80
134
|
|
81
135
|
s = cf.get_stack(state[:stack_name])
|
82
|
-
while s.stack_status == '
|
136
|
+
while s.stack_status == 'UPDATE_IN_PROGRESS'
|
83
137
|
debug_stack_events(state[:stack_name])
|
84
|
-
info("CloudFormation waiting for stack <#{state[:stack_name]}> to be
|
85
|
-
sleep(
|
138
|
+
info("CloudFormation waiting for stack <#{state[:stack_name]}> to be updated.....")
|
139
|
+
sleep(20)
|
86
140
|
s = cf.get_stack(state[:stack_name])
|
87
141
|
end
|
88
142
|
display_stack_events(state[:stack_name])
|
89
|
-
if s.stack_status == '
|
143
|
+
if s.stack_status == 'UPDATE_COMPLETE'
|
90
144
|
outputs = Hash[*s.outputs.map do |o|
|
91
145
|
[o[:output_key], o[:output_value]]
|
92
146
|
end.flatten]
|
93
147
|
state[:hostname] = config[:hostname].gsub(/\${([^}]+)}/) { outputs[Regexp.last_match(1)] || '' } if config[:hostname]
|
94
|
-
info("CloudFormation stack <#{state[:stack_name]}>
|
148
|
+
info("CloudFormation stack <#{state[:stack_name]}> updated.")
|
95
149
|
else
|
96
|
-
error("CloudFormation stack <#{stack.stack_name}> failed to
|
97
|
-
destroy(state)
|
150
|
+
error("CloudFormation stack <#{stack.stack_name}> failed to update.")
|
98
151
|
end
|
99
152
|
end
|
100
153
|
|
@@ -163,6 +216,48 @@ module Kitchen
|
|
163
216
|
cf.create_stack(stack_data)
|
164
217
|
end
|
165
218
|
|
219
|
+
def stack_exists
|
220
|
+
s = cf.get_stack(config[:stack_name])
|
221
|
+
info("Stack #{s.stack_name} already exists so not creating") if s.exists?
|
222
|
+
s.exists?
|
223
|
+
end
|
224
|
+
|
225
|
+
def change_set_exists
|
226
|
+
s = cf.describe_change_set(config[:stack_name], config[:change_set_name])
|
227
|
+
if s.nil? || s.status == 'DELETE_COMPLETE'
|
228
|
+
false
|
229
|
+
else
|
230
|
+
info("Change Set #{s[:change_set_name]} already exists so not creating")
|
231
|
+
true
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def create_change_set
|
236
|
+
stack_data = stack_generator.cf_stack_data
|
237
|
+
stack_data[:change_set_name] = config[:change_set_name] if config[:change_set_name]
|
238
|
+
stack_data[:change_set_type] = config[:change_set_type] if config[:change_set_type] # accepts CREATE, UPDATE
|
239
|
+
info("Creating Change Set for CloudFormation Stack #{stack_data[:stack_name]}")
|
240
|
+
stack_data[:template_url] = config[:change_set_template_url] if config[:change_set_template_file]
|
241
|
+
if config[:change_set_template_file]
|
242
|
+
stack_data[:template_body] = File.open(config[:change_set_template_file], 'rb') { |file| file.read }
|
243
|
+
end
|
244
|
+
cf.create_change_set(stack_data)
|
245
|
+
end
|
246
|
+
|
247
|
+
def execute_change_set
|
248
|
+
stack_data = {}
|
249
|
+
stack_data[:stack_name] = config[:stack_name]
|
250
|
+
stack_data[:change_set_name] = config[:change_set_name]
|
251
|
+
info("Execute Change Set #{stack_data[:change_set_name]} for CloudFormation Stack #{stack_data[:stack_name]}")
|
252
|
+
cf.execute_change_set(stack_data)
|
253
|
+
end
|
254
|
+
|
255
|
+
def update_stack
|
256
|
+
stack_data = stack_generator.cf_stack_data
|
257
|
+
info("Updating CloudFormation Stack #{stack_data[:stack_name]}")
|
258
|
+
cf.update_stack(stack_data)
|
259
|
+
end
|
260
|
+
|
166
261
|
def debug_stack_events(stack_name)
|
167
262
|
return unless logger.debug?
|
168
263
|
response = cf.get_stack_events(stack_name)
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
require 'kitchen'
|
17
|
+
# require "kitchen/cloudformation/configurable"
|
18
|
+
|
19
|
+
# The design of the provisioner is unconventional compared to other Test Kitchen provisioner plugins. Since Cloudformation
|
20
|
+
# creates and provisions resources when applying an execution plan, managed by the driver, the provisioner simply
|
21
|
+
# proxies the driver's create action to apply any changes to the existing Cloudformation state.
|
22
|
+
#
|
23
|
+
# === Configuration
|
24
|
+
#
|
25
|
+
# ==== Example .kitchen.yml snippet
|
26
|
+
#
|
27
|
+
# provisioner:
|
28
|
+
# name: cloudformation
|
29
|
+
#
|
30
|
+
# @see ::Kitchen::Driver::Cloudformation
|
31
|
+
# rubocop:disable Style/ClassAndModuleChildren
|
32
|
+
class ::Kitchen::Provisioner::Cloudformation < ::Kitchen::Provisioner::Base
|
33
|
+
kitchen_provisioner_api_version 2
|
34
|
+
|
35
|
+
# include ::Kitchen::Cloudformation::Configurable
|
36
|
+
|
37
|
+
# Proxies the driver's create action.
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# `kitchen converge suite-name`
|
41
|
+
# @param state [::Hash] the mutable instance and provisioner state.
|
42
|
+
# @raise [::Kitchen::ActionFailed] if the result of the action is a failure.
|
43
|
+
def call(state)
|
44
|
+
info("State is <#{state}>.")
|
45
|
+
instance.driver.update state
|
46
|
+
end
|
47
|
+
end
|
metadata
CHANGED
@@ -1,35 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kitchen-cloudformation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Neill Turner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-06-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: aws-sdk
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
20
|
-
- - ">="
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: 1.4.1
|
19
|
+
version: '3'
|
23
20
|
type: :runtime
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
26
23
|
requirements:
|
27
24
|
- - "~>"
|
28
25
|
- !ruby/object:Gem::Version
|
29
|
-
version: '
|
30
|
-
- - ">="
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: 1.4.1
|
26
|
+
version: '3'
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
28
|
name: excon
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -59,33 +53,33 @@ dependencies:
|
|
59
53
|
- !ruby/object:Gem::Version
|
60
54
|
version: '0'
|
61
55
|
- !ruby/object:Gem::Dependency
|
62
|
-
name:
|
56
|
+
name: retryable
|
63
57
|
requirement: !ruby/object:Gem::Requirement
|
64
58
|
requirements:
|
65
59
|
- - "~>"
|
66
60
|
- !ruby/object:Gem::Version
|
67
|
-
version: '2'
|
61
|
+
version: '2.0'
|
68
62
|
type: :runtime
|
69
63
|
prerelease: false
|
70
64
|
version_requirements: !ruby/object:Gem::Requirement
|
71
65
|
requirements:
|
72
66
|
- - "~>"
|
73
67
|
- !ruby/object:Gem::Version
|
74
|
-
version: '2'
|
68
|
+
version: '2.0'
|
75
69
|
- !ruby/object:Gem::Dependency
|
76
|
-
name:
|
70
|
+
name: test-kitchen
|
77
71
|
requirement: !ruby/object:Gem::Requirement
|
78
72
|
requirements:
|
79
|
-
- - "
|
73
|
+
- - ">="
|
80
74
|
- !ruby/object:Gem::Version
|
81
|
-
version:
|
75
|
+
version: 1.4.1
|
82
76
|
type: :runtime
|
83
77
|
prerelease: false
|
84
78
|
version_requirements: !ruby/object:Gem::Requirement
|
85
79
|
requirements:
|
86
|
-
- - "
|
80
|
+
- - ">="
|
87
81
|
- !ruby/object:Gem::Version
|
88
|
-
version:
|
82
|
+
version: 1.4.1
|
89
83
|
description: A Test Kitchen Driver for Amazon AWS CloudFormation
|
90
84
|
email:
|
91
85
|
- neillwturner@gmail.com
|
@@ -102,6 +96,7 @@ files:
|
|
102
96
|
- lib/kitchen/driver/aws/stack_generator.rb
|
103
97
|
- lib/kitchen/driver/cloudformation.rb
|
104
98
|
- lib/kitchen/driver/cloudformation_version.rb
|
99
|
+
- lib/kitchen/provisioner/cloudformation.rb
|
105
100
|
homepage: https://github.com/neillturner/kitchen-cloudformation
|
106
101
|
licenses:
|
107
102
|
- Apache-2.0
|
@@ -121,8 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
116
|
- !ruby/object:Gem::Version
|
122
117
|
version: '0'
|
123
118
|
requirements: []
|
124
|
-
|
125
|
-
rubygems_version: 2.6.13
|
119
|
+
rubygems_version: 3.1.4
|
126
120
|
signing_key:
|
127
121
|
specification_version: 4
|
128
122
|
summary: A Test Kitchen Driver for AWS CloudFormation
|