kitchen-terraform 0.1.0

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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +1 -0
  3. data.tar.gz.sig +0 -0
  4. data/lib/kitchen/driver/terraform.rb +57 -0
  5. data/lib/kitchen/provisioner/terraform.rb +58 -0
  6. data/lib/kitchen/verifier/terraform.rb +109 -0
  7. data/lib/terraform/apply_command.rb +32 -0
  8. data/lib/terraform/client.rb +102 -0
  9. data/lib/terraform/client_holder.rb +26 -0
  10. data/lib/terraform/command.rb +58 -0
  11. data/lib/terraform/command_options.rb +45 -0
  12. data/lib/terraform/error.rb +20 -0
  13. data/lib/terraform/get_command.rb +32 -0
  14. data/lib/terraform/inspec_runner.rb +44 -0
  15. data/lib/terraform/invalid_version.rb +34 -0
  16. data/lib/terraform/output_command.rb +38 -0
  17. data/lib/terraform/output_not_found.rb +23 -0
  18. data/lib/terraform/plan_command.rb +35 -0
  19. data/lib/terraform/validate_command.rb +32 -0
  20. data/lib/terraform/version.rb +19 -0
  21. data/lib/terraform/version_command.rb +32 -0
  22. data/spec/lib/kitchen/driver/terraform_spec.rb +126 -0
  23. data/spec/lib/kitchen/provisioner/terraform_spec.rb +106 -0
  24. data/spec/lib/kitchen/verifier/terraform_spec.rb +302 -0
  25. data/spec/lib/terraform/apply_command_spec.rb +32 -0
  26. data/spec/lib/terraform/client_spec.rb +211 -0
  27. data/spec/lib/terraform/command_options_spec.rb +34 -0
  28. data/spec/lib/terraform/get_command_spec.rb +30 -0
  29. data/spec/lib/terraform/inspec_runner_spec.rb +67 -0
  30. data/spec/lib/terraform/output_command_spec.rb +58 -0
  31. data/spec/lib/terraform/plan_command_spec.rb +46 -0
  32. data/spec/lib/terraform/validate_command_spec.rb +30 -0
  33. data/spec/lib/terraform/version_command_spec.rb +30 -0
  34. data/spec/spec_helper.rb +27 -0
  35. data/spec/support/coverage.rb +21 -0
  36. data/spec/support/terraform/client_holder_context.rb +26 -0
  37. data/spec/support/terraform/client_holder_examples.rb +36 -0
  38. data/spec/support/terraform/command_examples.rb +80 -0
  39. data/spec/support/terraform/versions_are_set_examples.rb +35 -0
  40. metadata +329 -0
  41. metadata.gz.sig +2 -0
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2016 New Context Services, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'kitchen/provisioner/terraform'
18
+ require 'terraform/error'
19
+ require 'support/terraform/client_holder_context'
20
+ require 'support/terraform/client_holder_examples'
21
+ require 'support/terraform/versions_are_set_examples'
22
+
23
+ RSpec.describe Kitchen::Provisioner::Terraform do
24
+ let(:described_instance) { described_class.new kitchen_root: kitchen_root }
25
+
26
+ let(:kitchen_root) { '<kitchen_root>' }
27
+
28
+ it_behaves_like Terraform::ClientHolder
29
+
30
+ it_behaves_like 'versions are set'
31
+
32
+ describe '#call(_state = nil)' do
33
+ include_context '#client'
34
+
35
+ let(:call_method) { described_instance.call }
36
+
37
+ context 'when the configuration can be applied' do
38
+ before do
39
+ allow(client).to receive(:validate_configuration_files).with no_args
40
+
41
+ allow(client).to receive(:download_modules).with no_args
42
+
43
+ allow(client).to receive(:plan_execution).with no_args
44
+
45
+ allow(client).to receive(:apply_execution_plan).with no_args
46
+ end
47
+
48
+ after { call_method }
49
+
50
+ subject { client }
51
+
52
+ it 'validates the configuration files' do
53
+ is_expected.to receive(:validate_configuration_files).with no_args
54
+ end
55
+
56
+ it 'downloads the modules' do
57
+ is_expected.to receive(:download_modules).with no_args
58
+ end
59
+
60
+ it 'plans the execution' do
61
+ is_expected.to receive(:plan_execution).with no_args
62
+ end
63
+
64
+ it 'applies the execution plan' do
65
+ is_expected.to receive(:apply_execution_plan).with no_args
66
+ end
67
+ end
68
+
69
+ context 'when the configuration can not be applied due to failed command' do
70
+ before do
71
+ allow(client).to receive(:validate_configuration_files)
72
+ .and_raise Terraform::Error
73
+ end
74
+
75
+ subject { proc { call_method } }
76
+
77
+ it('raises an error') { is_expected.to raise_error Kitchen::ActionFailed }
78
+ end
79
+ end
80
+
81
+ describe '#directory' do
82
+ subject { described_instance.directory.to_s }
83
+
84
+ it 'defaults to the Test Kitchen root directory' do
85
+ is_expected.to eq kitchen_root
86
+ end
87
+ end
88
+
89
+ describe '#kitchen_root' do
90
+ subject { described_instance.kitchen_root.to_s }
91
+
92
+ it('is the Test Kitchen root directory') { is_expected.to eq kitchen_root }
93
+ end
94
+
95
+ describe '#variable_files' do
96
+ subject { described_instance.variable_files }
97
+
98
+ it('defaults to empty array') { is_expected.to eq [] }
99
+ end
100
+
101
+ describe '#variables' do
102
+ subject { described_instance.variables }
103
+
104
+ it('defaults to empty array') { is_expected.to eq [] }
105
+ end
106
+ end
@@ -0,0 +1,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2016 New Context Services, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'inspec'
18
+ require 'kitchen/verifier/terraform'
19
+ require 'support/terraform/client_holder_context'
20
+ require 'support/terraform/client_holder_examples'
21
+ require 'support/terraform/versions_are_set_examples'
22
+
23
+ RSpec.describe Kitchen::Verifier::Terraform do
24
+ let(:config) { { kitchen_root: '<kitchen_root>' } }
25
+
26
+ let(:described_instance) { described_class.new config }
27
+
28
+ let(:state) { {} }
29
+
30
+ shared_context '#instance' do
31
+ let(:instance) { instance_double Kitchen::Instance }
32
+
33
+ let(:logger) { instance_double Kitchen::Logger }
34
+
35
+ let(:transport) { Kitchen::Transport::Ssh.new }
36
+
37
+ let(:transport_config) { {} }
38
+
39
+ before do
40
+ described_instance.finalize_config! instance
41
+
42
+ allow(instance).to receive(:logger).with(no_args).and_return logger
43
+
44
+ allow(instance).to receive(:transport).with(no_args).and_return transport
45
+
46
+ allow(logger).to receive :info
47
+
48
+ allow(transport).to receive(:config).with(no_args)
49
+ .and_return Kitchen::LazyHash
50
+ .new(transport_config, instance_double(Object))
51
+
52
+ allow(transport).to receive(:diagnose).with(no_args)
53
+ .and_return transport_config
54
+
55
+ allow(transport).to receive(:name).with(no_args)
56
+ .and_return instance_double Object
57
+ end
58
+ end
59
+
60
+ it_behaves_like Terraform::ClientHolder
61
+
62
+ it_behaves_like 'versions are set'
63
+
64
+ describe '#attributes(group:)' do
65
+ subject { described_instance.attributes group: {} }
66
+
67
+ it('defaults to an empty hash') { is_expected.to eq({}) }
68
+ end
69
+
70
+ describe '#call(state)' do
71
+ include_context '#client'
72
+
73
+ let(:call_method) { described_instance.call state }
74
+
75
+ let(:config) { { groups: [group] } }
76
+
77
+ let(:group) { { hostnames: hostnames } }
78
+
79
+ let(:hostnames) { instance_double Object }
80
+
81
+ context 'when the hostnames list output can be extracted' do
82
+ let(:output) { instance_double Object }
83
+
84
+ before do
85
+ allow(client).to receive(:extract_list_output).with(name: hostnames)
86
+ .and_yield output
87
+ end
88
+
89
+ after { call_method }
90
+
91
+ subject { described_instance }
92
+
93
+ it 'verifies the hosts of each group' do
94
+ is_expected.to receive(:verify).with group: group, hostnames: output,
95
+ state: state
96
+ end
97
+ end
98
+
99
+ context 'when the hostnames list output can not be extracted' do
100
+ before do
101
+ allow(client).to receive(:extract_list_output).with(name: hostnames)
102
+ .and_raise Terraform::Error
103
+ end
104
+
105
+ subject { proc { call_method } }
106
+
107
+ it('raises an error') { is_expected.to raise_error Kitchen::ActionFailed }
108
+ end
109
+ end
110
+
111
+ describe '#controls(group:)' do
112
+ subject { described_instance.controls group: {} }
113
+
114
+ it('defaults to an empty array') { is_expected.to eq [] }
115
+ end
116
+
117
+ describe '#evaluate(exit_code:)' do
118
+ subject { proc { described_instance.evaluate exit_code: exit_code } }
119
+
120
+ context 'when the exit code is 0' do
121
+ let(:exit_code) { 0 }
122
+
123
+ it('does not raise an error') { is_expected.to_not raise_error }
124
+ end
125
+
126
+ context 'when the exit code is not 0' do
127
+ let(:exit_code) { 1 }
128
+
129
+ it('raises an error') { is_expected.to raise_error RuntimeError }
130
+ end
131
+ end
132
+
133
+ describe '#groups' do
134
+ subject { described_instance.groups }
135
+
136
+ it('defaults to an empty array') { is_expected.to eq [] }
137
+ end
138
+
139
+ describe '#initialize_runner(group:, hostname:, state:)' do
140
+ let(:group) { instance_double Object }
141
+
142
+ let(:hostname) { instance_double Object }
143
+
144
+ let(:inspec_runner) { instance_double Terraform::InspecRunner }
145
+
146
+ let :inspec_runner_class do
147
+ class_double(Terraform::InspecRunner).as_stubbed_const
148
+ end
149
+
150
+ let(:name) { instance_double Object }
151
+
152
+ let(:options) { instance_double Object }
153
+
154
+ let(:state) { instance_double Object }
155
+
156
+ let(:value) { instance_double Object }
157
+
158
+ before do
159
+ allow(described_instance).to receive(:runner_options_for_terraform)
160
+ .with(group: group, hostname: hostname, state: state).and_return options
161
+
162
+ allow(inspec_runner_class).to receive(:new).with(options)
163
+ .and_yield inspec_runner
164
+
165
+ allow(described_instance).to receive(:resolve_attributes)
166
+ .with(group: group).and_yield name, value
167
+
168
+ allow(inspec_runner).to receive(:define_attribute).with name: name,
169
+ value: value
170
+
171
+ allow(described_instance).to receive(:collect_tests).and_return []
172
+
173
+ allow(inspec_runner).to receive(:add).with targets: kind_of(Array)
174
+ end
175
+
176
+ subject do
177
+ lambda do |block|
178
+ described_instance.initialize_runner group: group, hostname: hostname,
179
+ state: state, &block
180
+ end
181
+ end
182
+
183
+ it 'yields an InspecRunner for the group host' do
184
+ is_expected.to yield_with_args inspec_runner
185
+ end
186
+ end
187
+
188
+ describe '#port(group:)' do
189
+ include_context '#instance'
190
+
191
+ let(:group) { {} }
192
+
193
+ let(:port) { instance_double Object }
194
+
195
+ let(:transport_config) { { port: port } }
196
+
197
+ subject { described_instance.port group: group }
198
+
199
+ it 'defaults to the transport port configuration' do
200
+ is_expected.to be port
201
+ end
202
+ end
203
+
204
+ describe '#resolve_attributes(group:)' do
205
+ include_context '#client'
206
+
207
+ let(:method_name) { instance_double Object }
208
+
209
+ let(:output) { instance_double Object }
210
+
211
+ let(:variable_name) { instance_double Object }
212
+
213
+ before do
214
+ allow(client).to receive(:extract_output).with(name: variable_name)
215
+ .and_yield output
216
+ end
217
+
218
+ subject do
219
+ lambda do |block|
220
+ described_instance.resolve_attributes group:
221
+ { attributes: { method_name => variable_name } }, &block
222
+ end
223
+ end
224
+
225
+ it 'extracts an output value for each attribute pair in the group' do
226
+ is_expected.to yield_with_args method_name, output
227
+ end
228
+ end
229
+
230
+ describe '#runner_options_for_terraform(group:, hostname:, state:)' do
231
+ include_context '#instance'
232
+
233
+ let(:controls) { instance_double Object }
234
+
235
+ let(:group) { { controls: controls, port: port, username: username } }
236
+
237
+ let(:hostname) { instance_double Object }
238
+
239
+ let(:port) { instance_double Object }
240
+
241
+ let(:username) { instance_double Object }
242
+
243
+ before do
244
+ allow(described_instance).to receive(:runner_options)
245
+ .with(transport, state).and_return({})
246
+ end
247
+
248
+ subject do
249
+ described_instance.runner_options_for_terraform group: group,
250
+ hostname: hostname,
251
+ state: state
252
+ end
253
+
254
+ it 'adds the group controls, host, port and user to the runner options' do
255
+ is_expected.to include controls: controls, host: hostname, port: port,
256
+ user: username
257
+ end
258
+ end
259
+
260
+ describe '#username(group:)' do
261
+ include_context '#instance'
262
+
263
+ let(:transport_config) { { username: username } }
264
+
265
+ let(:username) { instance_double Object }
266
+
267
+ before do
268
+ allow(transport_config).to receive(:fetch).with(:username)
269
+ .and_return username
270
+ end
271
+
272
+ subject { described_instance.username group: {} }
273
+
274
+ it('defaults to the transport username') { is_expected.to be username }
275
+ end
276
+
277
+ describe '#verify(group:, hostnames:, state:)' do
278
+ let(:group) { { name: instance_double(Object) } }
279
+
280
+ let(:hostname) { instance_double Object }
281
+
282
+ let(:runner) { instance_double Terraform::InspecRunner }
283
+
284
+ before do
285
+ allow(described_instance).to receive(:initialize_runner)
286
+ .with(group: group, hostname: hostname, state: state).and_yield runner
287
+
288
+ allow(runner).to receive(:verify_run).with verifier: described_instance
289
+ end
290
+
291
+ after do
292
+ described_instance.verify group: group, hostnames: [hostname],
293
+ state: state
294
+ end
295
+
296
+ subject { runner }
297
+
298
+ it 'verifies the Inspec run' do
299
+ is_expected.to receive(:verify_run).with verifier: described_instance
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2016 New Context Services, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'terraform/apply_command'
18
+ require 'support/terraform/command_examples'
19
+
20
+ RSpec.describe Terraform::ApplyCommand do
21
+ it_behaves_like Terraform::Command do
22
+ let(:command_options) { "-input=false -state=#{state}" }
23
+
24
+ let(:described_instance) { described_class.new state: state, plan: target }
25
+
26
+ let(:name) { 'apply' }
27
+
28
+ let(:state) { '<state_pathname>' }
29
+
30
+ let(:target) { '<plan_pathname>' }
31
+ end
32
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2016 New Context Services, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'kitchen/provisioner/terraform'
18
+ require 'terraform/client'
19
+ require 'terraform/command'
20
+
21
+ RSpec.describe Terraform::Client do
22
+ let(:described_instance) { described_class.new instance: instance }
23
+
24
+ let(:instance) { instance_double Kitchen::Instance }
25
+
26
+ let :instance_directory do
27
+ "#{kitchen_root}/.kitchen/kitchen-terraform/#{instance_name}"
28
+ end
29
+
30
+ let(:instance_name) { '<instance_name>' }
31
+
32
+ let(:kitchen_root) { '<kitchen_root>' }
33
+
34
+ let :provisioner do
35
+ Kitchen::Provisioner::Terraform.new kitchen_root: kitchen_root
36
+ end
37
+
38
+ before do
39
+ allow(instance).to receive(:name).with(no_args).and_return instance_name
40
+
41
+ allow(instance).to receive(:provisioner).with(no_args)
42
+ .and_return provisioner
43
+ end
44
+
45
+ describe '#apply_execution_plan' do
46
+ after { described_instance.apply_execution_plan }
47
+
48
+ subject { described_instance }
49
+
50
+ it 'applies the plan to the state' do
51
+ is_expected.to receive(:run)
52
+ .with command_class: Terraform::ApplyCommand,
53
+ state: described_instance.state_pathname,
54
+ plan: described_instance.plan_pathname
55
+ end
56
+ end
57
+
58
+ describe '#download_modules' do
59
+ after { described_instance.download_modules }
60
+
61
+ subject { described_instance }
62
+
63
+ it 'downloads the modules required in the directory' do
64
+ is_expected.to receive(:run).with command_class: Terraform::GetCommand,
65
+ dir: described_instance.directory
66
+ end
67
+ end
68
+
69
+ describe '#extract_list_output(name:)' do
70
+ let(:name) { instance_double Object }
71
+
72
+ let(:output) { 'foo,bar' }
73
+
74
+ before do
75
+ allow(described_instance).to receive(:extract_output).with(name: name)
76
+ .and_yield output
77
+ end
78
+
79
+ subject do
80
+ ->(block) { described_instance.extract_list_output name: name, &block }
81
+ end
82
+
83
+ it 'splits and yields the extracted comma seperated output' do
84
+ is_expected.to yield_with_args %w(foo bar)
85
+ end
86
+ end
87
+
88
+ describe '#extract_output(name:)' do
89
+ let(:name) { instance_double Object }
90
+
91
+ let(:output) { "foo\n" }
92
+
93
+ before do
94
+ allow(described_instance).to receive(:run).with(
95
+ command_class: Terraform::OutputCommand,
96
+ state: described_instance.state_pathname, name: name
97
+ ).and_yield output
98
+ end
99
+
100
+ subject do
101
+ ->(block) { described_instance.extract_output name: name, &block }
102
+ end
103
+
104
+ it 'chomps and yields the extracted output from the state' do
105
+ is_expected.to yield_with_args 'foo'
106
+ end
107
+ end
108
+
109
+ describe '#fetch_version' do
110
+ let(:output) { instance_double Object }
111
+
112
+ before do
113
+ allow(described_instance).to receive(:run)
114
+ .with(command_class: Terraform::VersionCommand).and_yield output
115
+ end
116
+
117
+ subject { ->(block) { described_instance.fetch_version(&block) } }
118
+
119
+ it 'yields the Terraform version' do
120
+ is_expected.to yield_with_args output
121
+ end
122
+ end
123
+
124
+ describe '#instance_directory' do
125
+ subject { described_instance.instance_directory.to_s }
126
+
127
+ it { is_expected.to eq instance_directory }
128
+ end
129
+
130
+ describe '#plan_destructive_execution' do
131
+ after { described_instance.plan_destructive_execution }
132
+
133
+ subject { described_instance }
134
+
135
+ it 'plans a destructive execution against the state' do
136
+ is_expected.to receive(:run)
137
+ .with command_class: Terraform::PlanCommand, destroy: true,
138
+ out: described_instance.plan_pathname,
139
+ state: described_instance.state_pathname,
140
+ var: described_instance.variables,
141
+ var_file: described_instance.variable_files,
142
+ dir: described_instance.directory
143
+ end
144
+ end
145
+
146
+ describe '#plan_execution' do
147
+ after { described_instance.plan_execution }
148
+
149
+ subject { described_instance }
150
+
151
+ it 'plans an execution against the state' do
152
+ is_expected.to receive(:run)
153
+ .with command_class: Terraform::PlanCommand, destroy: false,
154
+ out: described_instance.plan_pathname,
155
+ state: described_instance.state_pathname,
156
+ var: described_instance.variables,
157
+ var_file: described_instance.variable_files,
158
+ dir: described_instance.directory
159
+ end
160
+ end
161
+
162
+ describe '#plan_pathname' do
163
+ subject { described_instance.plan_pathname.to_s }
164
+
165
+ it { is_expected.to eq "#{instance_directory}/terraform.tfplan" }
166
+ end
167
+
168
+ describe '#run(command_class:, **parameters)' do
169
+ let(:command) { instance_double Terraform::Command }
170
+
171
+ let(:command_class) { Class.new.include Terraform::Command }
172
+
173
+ let(:output) { instance_double Object }
174
+
175
+ let(:parameters) { { foo: 'bar' } }
176
+
177
+ before do
178
+ allow(command_class).to receive(:new).with(**parameters)
179
+ .and_yield command
180
+
181
+ allow(command).to receive(:execute).with(no_args).and_yield output
182
+ end
183
+
184
+ subject do
185
+ lambda do |block|
186
+ described_instance.run command_class: command_class, **parameters,
187
+ &block
188
+ end
189
+ end
190
+
191
+ it('yields the command output') { is_expected.to yield_with_args output }
192
+ end
193
+
194
+ describe '#state_pathname' do
195
+ subject { described_instance.state_pathname.to_s }
196
+
197
+ it { is_expected.to eq "#{instance_directory}/terraform.tfstate" }
198
+ end
199
+
200
+ describe '#validate_configuration_files' do
201
+ after { described_instance.validate_configuration_files }
202
+
203
+ subject { described_instance }
204
+
205
+ it 'validates the configuration files in the directory' do
206
+ is_expected.to receive(:run)
207
+ .with command_class: Terraform::ValidateCommand,
208
+ dir: described_instance.directory
209
+ end
210
+ end
211
+ end