test-kitchen 1.7.3 → 1.8.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.
- checksums.yaml +4 -4
- data/.kitchen.ci.yml +3 -0
- data/CHANGELOG.md +8 -0
- data/lib/kitchen/data_munger.rb +4 -1
- data/lib/kitchen/provisioner/chef/common_sandbox.rb +46 -2
- data/lib/kitchen/provisioner/chef/policyfile.rb +107 -0
- data/lib/kitchen/provisioner/chef_base.rb +38 -1
- data/lib/kitchen/provisioner/chef_zero.rb +10 -0
- data/lib/kitchen/transport/winrm.rb +55 -7
- data/lib/kitchen/version.rb +1 -1
- data/spec/kitchen/data_munger_spec.rb +68 -0
- data/spec/kitchen/provisioner/chef_base_spec.rb +164 -0
- data/spec/kitchen/transport/winrm_spec.rb +153 -0
- data/test-kitchen.gemspec +1 -0
- data/testing_windows.md +1 -0
- metadata +18 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2ad48e5953da8eb9d58bd3a8e2933450d669caa8
|
4
|
+
data.tar.gz: 9d661157b18854c43bb1ce3094cb95888b9e0f95
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 61f25a72986488f05691bbbcb1189e08bac9a6953c52bbc93980c0f7caa283ac80a197da1a296e273217d99f43115f1cecfc136ccc43804eefc985a97e170894
|
7
|
+
data.tar.gz: 344538d21ba1ce73e9c892bb895ad59f31a078eb661552ef8661d6c878f5d4f187eee6eb5f13756eb81fd894f2ee632a1a360fa87a9452fb8f06f3a4bb5ba3dd
|
data/.kitchen.ci.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## [1.8.0](https://github.com/test-kitchen/test-kitchen/tree/1.8.0) (2016-05-05)
|
4
|
+
[Full Changelog](https://github.com/test-kitchen/test-kitchen/compare/v1.7.3...1.8.0)
|
5
|
+
|
6
|
+
**Implemented enhancements:**
|
7
|
+
|
8
|
+
- Add native policyfile resolution support [\#1014](https://github.com/test-kitchen/test-kitchen/pull/1014) ([danielsdeleo](https://github.com/danielsdeleo))
|
9
|
+
- Provide the option to run all winrm commands through a scheduled task [\#1012](https://github.com/test-kitchen/test-kitchen/pull/1012) ([mwrock](https://github.com/mwrock))
|
10
|
+
|
3
11
|
## [1.7.3](https://github.com/test-kitchen/test-kitchen/tree/1.7.3) (2016-04-13)
|
4
12
|
[Full Changelog](https://github.com/test-kitchen/test-kitchen/compare/v1.7.2...1.7.3)
|
5
13
|
|
data/lib/kitchen/data_munger.rb
CHANGED
@@ -624,10 +624,11 @@ module Kitchen
|
|
624
624
|
# Destructively moves key Chef configuration key/value pairs from being
|
625
625
|
# directly under a suite or platform into a `:provisioner` sub-hash.
|
626
626
|
#
|
627
|
-
# There are
|
627
|
+
# There are three key Chef configuration key/value pairs:
|
628
628
|
#
|
629
629
|
# 1. `:attributes`
|
630
630
|
# 2. `:run_list`
|
631
|
+
# 3. `:named_run_list`
|
631
632
|
#
|
632
633
|
# This method converts the following:
|
633
634
|
#
|
@@ -678,11 +679,13 @@ module Kitchen
|
|
678
679
|
data.fetch(:suites, []).each do |suite|
|
679
680
|
move_chef_data_to_provisioner_at!(suite, :attributes)
|
680
681
|
move_chef_data_to_provisioner_at!(suite, :run_list)
|
682
|
+
move_chef_data_to_provisioner_at!(suite, :named_run_list)
|
681
683
|
end
|
682
684
|
|
683
685
|
data.fetch(:platforms, []).each do |platform|
|
684
686
|
move_chef_data_to_provisioner_at!(platform, :attributes)
|
685
687
|
move_chef_data_to_provisioner_at!(platform, :run_list)
|
688
|
+
move_chef_data_to_provisioner_at!(platform, :named_run_list)
|
686
689
|
end
|
687
690
|
end
|
688
691
|
|
@@ -16,6 +16,8 @@
|
|
16
16
|
# See the License for the specific language governing permissions and
|
17
17
|
# limitations under the License.
|
18
18
|
|
19
|
+
require "json"
|
20
|
+
|
19
21
|
module Kitchen
|
20
22
|
|
21
23
|
module Provisioner
|
@@ -86,6 +88,14 @@ module Kitchen
|
|
86
88
|
select { |fn| File.file?(fn) && ! %w[. ..].include?(fn) }
|
87
89
|
end
|
88
90
|
|
91
|
+
# @return [String] an absolute path to a Policyfile, relative to the
|
92
|
+
# kitchen root
|
93
|
+
# @api private
|
94
|
+
def policyfile
|
95
|
+
basename = config[:policyfile_path] || "Policyfile.rb"
|
96
|
+
File.join(config[:kitchen_root], basename)
|
97
|
+
end
|
98
|
+
|
89
99
|
# @return [String] an absolute path to a Berksfile, relative to the
|
90
100
|
# kitchen root
|
91
101
|
# @api private
|
@@ -246,8 +256,11 @@ module Kitchen
|
|
246
256
|
# Prepares Chef cookbooks for inclusion in the sandbox path.
|
247
257
|
#
|
248
258
|
# @api private
|
259
|
+
# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
249
260
|
def prepare_cookbooks
|
250
|
-
if File.exist?(
|
261
|
+
if File.exist?(policyfile)
|
262
|
+
resolve_with_policyfile
|
263
|
+
elsif File.exist?(berksfile)
|
251
264
|
resolve_with_berkshelf
|
252
265
|
elsif File.exist?(cheffile)
|
253
266
|
resolve_with_librarian
|
@@ -268,7 +281,11 @@ module Kitchen
|
|
268
281
|
#
|
269
282
|
# @api private
|
270
283
|
def prepare_json
|
271
|
-
dna =
|
284
|
+
dna = if File.exist?(policyfile)
|
285
|
+
update_dna_for_policyfile
|
286
|
+
else
|
287
|
+
config[:attributes].merge(:run_list => config[:run_list])
|
288
|
+
end
|
272
289
|
|
273
290
|
info("Preparing dna.json")
|
274
291
|
debug("Creating dna.json from #{dna.inspect}")
|
@@ -278,6 +295,32 @@ module Kitchen
|
|
278
295
|
end
|
279
296
|
end
|
280
297
|
|
298
|
+
def update_dna_for_policyfile
|
299
|
+
if !config[:run_list].nil? && !config[:run_list].empty?
|
300
|
+
warn("You must set your run_list in your policyfile instead of "\
|
301
|
+
"kitchen config. The run_list your config will be ignored.")
|
302
|
+
warn("Ignored run_list: #{config[:run_list].inspect}")
|
303
|
+
end
|
304
|
+
policylock = policyfile.gsub(/\.rb\Z/, ".lock.json")
|
305
|
+
unless File.exist?(policylock)
|
306
|
+
Kitchen.mutex.synchronize do
|
307
|
+
Chef::Policyfile.new(policyfile, sandbox_path, logger).compile
|
308
|
+
end
|
309
|
+
end
|
310
|
+
policy_name = JSON.parse(IO.read(policylock))["name"]
|
311
|
+
policy_group = "local"
|
312
|
+
config[:attributes].merge(:policy_name => policy_name, :policy_group => policy_group)
|
313
|
+
end
|
314
|
+
|
315
|
+
# Performs a Policyfile cookbook resolution inside a common mutex.
|
316
|
+
#
|
317
|
+
# @api private
|
318
|
+
def resolve_with_policyfile
|
319
|
+
Kitchen.mutex.synchronize do
|
320
|
+
Chef::Policyfile.new(policyfile, sandbox_path, logger).resolve
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
281
324
|
# Performs a Berkshelf cookbook resolution inside a common mutex.
|
282
325
|
#
|
283
326
|
# @api private
|
@@ -316,6 +359,7 @@ module Kitchen
|
|
316
359
|
def tmpsitebooks_dir
|
317
360
|
File.join(sandbox_path, "cookbooks")
|
318
361
|
end
|
362
|
+
|
319
363
|
end
|
320
364
|
end
|
321
365
|
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
|
4
|
+
#
|
5
|
+
# Copyright (C) 2013, Fletcher Nichol
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
|
19
|
+
require "kitchen/errors"
|
20
|
+
require "kitchen/logging"
|
21
|
+
require "kitchen/shell_out"
|
22
|
+
|
23
|
+
module Kitchen
|
24
|
+
|
25
|
+
module Provisioner
|
26
|
+
|
27
|
+
module Chef
|
28
|
+
|
29
|
+
# Chef cookbook resolver that uses Policyfiles to calculate dependencies.
|
30
|
+
#
|
31
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
32
|
+
class Policyfile
|
33
|
+
|
34
|
+
include Logging
|
35
|
+
include ShellOut
|
36
|
+
|
37
|
+
# Creates a new cookbook resolver.
|
38
|
+
#
|
39
|
+
# @param berksfile [String] path to a Berksfile
|
40
|
+
# @param path [String] path in which to vendor the resulting
|
41
|
+
# cookbooks
|
42
|
+
# @param logger [Kitchen::Logger] a logger to use for output, defaults
|
43
|
+
# to `Kitchen.logger`
|
44
|
+
def initialize(policyfile, path, logger = Kitchen.logger)
|
45
|
+
@policyfile = policyfile
|
46
|
+
@path = path
|
47
|
+
@logger = logger
|
48
|
+
end
|
49
|
+
|
50
|
+
# Loads the library code required to use the resolver.
|
51
|
+
#
|
52
|
+
# @param logger [Kitchen::Logger] a logger to use for output, defaults
|
53
|
+
# to `Kitchen.logger`
|
54
|
+
def self.load!(logger = Kitchen.logger)
|
55
|
+
detect_chef_command!(logger)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Performs the cookbook resolution and vendors the resulting cookbooks
|
59
|
+
# in the desired path.
|
60
|
+
def resolve
|
61
|
+
info("Exporting cookbook dependencies from Policyfile #{path}...")
|
62
|
+
run_command("chef export #{policyfile} #{path} --force")
|
63
|
+
end
|
64
|
+
|
65
|
+
# Runs `chef install` to determine the correct cookbook set and
|
66
|
+
# generate the policyfile lock.
|
67
|
+
def compile
|
68
|
+
info("Policy lock file doesn't exist, running `chef install` for "\
|
69
|
+
"Policyfile #{policyfile}...")
|
70
|
+
run_command("chef install #{policyfile}")
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# @return [String] path to a Berksfile
|
76
|
+
# @api private
|
77
|
+
attr_reader :policyfile
|
78
|
+
|
79
|
+
# @return [String] path in which to vendor the resulting cookbooks
|
80
|
+
# @api private
|
81
|
+
attr_reader :path
|
82
|
+
|
83
|
+
# @return [Kitchen::Logger] a logger to use for output
|
84
|
+
# @api private
|
85
|
+
attr_reader :logger
|
86
|
+
|
87
|
+
# Ensure the `chef` command is in the path.
|
88
|
+
#
|
89
|
+
# @param logger [Kitchen::Logger] the logger to use
|
90
|
+
# @raise [UserError] if the `chef` command is not in the PATH
|
91
|
+
# @api private
|
92
|
+
def self.detect_chef_command!(logger)
|
93
|
+
unless ENV["PATH"].split(File::PATH_SEPARATOR).any? { |p|
|
94
|
+
File.exist?(File.join(p, "chef"))
|
95
|
+
}
|
96
|
+
logger.fatal("The `chef` executable cannot be found in your " \
|
97
|
+
"PATH. Ensure you have installed ChefDK from " \
|
98
|
+
"https://downloads.chef.io and that your PATH " \
|
99
|
+
"setting includes the path to the `chef` comand.")
|
100
|
+
raise UserError,
|
101
|
+
"Could not find the chef executable in your PATH."
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -21,6 +21,7 @@ require "pathname"
|
|
21
21
|
require "json"
|
22
22
|
require "cgi"
|
23
23
|
|
24
|
+
require "kitchen/provisioner/chef/policyfile"
|
24
25
|
require "kitchen/provisioner/chef/berkshelf"
|
25
26
|
require "kitchen/provisioner/chef/common_sandbox"
|
26
27
|
require "kitchen/provisioner/chef/librarian"
|
@@ -53,6 +54,9 @@ module Kitchen
|
|
53
54
|
default_config :log_file, nil
|
54
55
|
default_config :log_level, "auto"
|
55
56
|
default_config :profile_ruby, false
|
57
|
+
# Will try to autodetect by searching for `Policyfile.rb` if not set.
|
58
|
+
# If set, will error if the file doesn't exist.
|
59
|
+
default_config :policyfile_path, nil
|
56
60
|
default_config :cookbook_files_glob, %w[
|
57
61
|
README.* metadata.{json,rb}
|
58
62
|
attributes/**/* definitions/**/* files/**/* libraries/**/*
|
@@ -114,6 +118,7 @@ module Kitchen
|
|
114
118
|
# (see Base#create_sandbox)
|
115
119
|
def create_sandbox
|
116
120
|
super
|
121
|
+
sanity_check_sandbox_options!
|
117
122
|
Chef::CommonSandbox.new(config, sandbox_path, instance).populate
|
118
123
|
end
|
119
124
|
|
@@ -158,6 +163,14 @@ module Kitchen
|
|
158
163
|
end
|
159
164
|
end
|
160
165
|
|
166
|
+
# @return [String] an absolute path to a Policyfile, relative to the
|
167
|
+
# kitchen root
|
168
|
+
# @api private
|
169
|
+
def policyfile
|
170
|
+
policyfile_basename = config[:policyfile_path] || "Policyfile.rb"
|
171
|
+
File.join(config[:kitchen_root], policyfile_basename)
|
172
|
+
end
|
173
|
+
|
161
174
|
# @return [String] an absolute path to a Berksfile, relative to the
|
162
175
|
# kitchen root
|
163
176
|
# @api private
|
@@ -264,7 +277,10 @@ module Kitchen
|
|
264
277
|
# (see Base#load_needed_dependencies!)
|
265
278
|
def load_needed_dependencies!
|
266
279
|
super
|
267
|
-
if File.exist?(
|
280
|
+
if File.exist?(policyfile)
|
281
|
+
debug("Policyfile found at #{policyfile}, using Policyfile to resolve dependencies")
|
282
|
+
Chef::Policyfile.load!(logger)
|
283
|
+
elsif File.exist?(berksfile)
|
268
284
|
debug("Berksfile found at #{berksfile}, loading Berkshelf")
|
269
285
|
Chef::Berkshelf.load!(logger)
|
270
286
|
elsif File.exist?(cheffile)
|
@@ -325,6 +341,27 @@ module Kitchen
|
|
325
341
|
config[:chef_omnibus_root] = installer.root
|
326
342
|
installer.install_command
|
327
343
|
end
|
344
|
+
|
345
|
+
def supports_policyfile?
|
346
|
+
false
|
347
|
+
end
|
348
|
+
|
349
|
+
# @return [void]
|
350
|
+
# @raise [UserError]
|
351
|
+
# @api private
|
352
|
+
def sanity_check_sandbox_options!
|
353
|
+
if config[:policyfile_path] && !File.exist?(policyfile)
|
354
|
+
raise UserError, "policyfile_path set in config "\
|
355
|
+
"(#{config[:policyfile_path]} could not be found. " \
|
356
|
+
"Expected to find it at full path #{policyfile} " \
|
357
|
+
end
|
358
|
+
if File.exist?(policyfile) && !supports_policyfile?
|
359
|
+
raise UserError, "policyfile detected, but provisioner " \
|
360
|
+
"#{self.class.name} doesn't support policyfiles. " \
|
361
|
+
"Either use a different provisioner, or delete/rename " \
|
362
|
+
"#{policyfile}"
|
363
|
+
end
|
364
|
+
end
|
328
365
|
end
|
329
366
|
end
|
330
367
|
end
|
@@ -32,6 +32,7 @@ module Kitchen
|
|
32
32
|
plugin_version Kitchen::VERSION
|
33
33
|
|
34
34
|
default_config :client_rb, {}
|
35
|
+
default_config :named_run_list, {}
|
35
36
|
default_config :json_attributes, true
|
36
37
|
default_config :chef_zero_host, nil
|
37
38
|
default_config :chef_zero_port, 8889
|
@@ -207,6 +208,7 @@ module Kitchen
|
|
207
208
|
# @api private
|
208
209
|
def prepare_client_rb
|
209
210
|
data = default_config_rb.merge(config[:client_rb])
|
211
|
+
data = data.merge(:named_run_list => config[:named_run_list]) if config[:named_run_list]
|
210
212
|
|
211
213
|
info("Preparing client.rb")
|
212
214
|
debug("Creating client.rb from #{data.inspect}")
|
@@ -240,6 +242,14 @@ module Kitchen
|
|
240
242
|
|
241
243
|
"#{chef_client_zero_env}\n#{sudo(ruby)} #{shim}"
|
242
244
|
end
|
245
|
+
|
246
|
+
# This provisioner supports policyfiles, so override the default (which
|
247
|
+
# is false)
|
248
|
+
# @return [true] always returns true
|
249
|
+
# @api private
|
250
|
+
def supports_policyfile?
|
251
|
+
true
|
252
|
+
end
|
243
253
|
end
|
244
254
|
end
|
245
255
|
end
|
@@ -42,6 +42,7 @@ module Kitchen
|
|
42
42
|
|
43
43
|
default_config :username, "administrator"
|
44
44
|
default_config :password, nil
|
45
|
+
default_config :elevated, false
|
45
46
|
default_config :rdp_port, 3389
|
46
47
|
default_config :connection_retries, 5
|
47
48
|
default_config :connection_retry_sleep, 1
|
@@ -185,6 +186,10 @@ module Kitchen
|
|
185
186
|
# @api private
|
186
187
|
attr_reader :winrm_transport
|
187
188
|
|
189
|
+
# @return [Boolean] whether to use winrm-elevated for running commands
|
190
|
+
# @api private
|
191
|
+
attr_reader :elevated
|
192
|
+
|
188
193
|
# Writes an RDP document to the local file system.
|
189
194
|
#
|
190
195
|
# @param opts [Hash] file options
|
@@ -217,13 +222,30 @@ module Kitchen
|
|
217
222
|
# script and the standard error stream
|
218
223
|
# @api private
|
219
224
|
def execute_with_exit_code(command)
|
220
|
-
|
221
|
-
|
225
|
+
if elevated
|
226
|
+
unless options[:elevated_username] == options[:user]
|
227
|
+
command = "$env:temp='#{unelevated_temp_dir}';#{command}"
|
228
|
+
end
|
229
|
+
response = elevated_runner.powershell_elevated(
|
230
|
+
command,
|
231
|
+
options[:elevated_username],
|
232
|
+
options[:elevated_password]
|
233
|
+
) do |stdout, _|
|
234
|
+
logger << stdout if stdout
|
235
|
+
end
|
236
|
+
else
|
237
|
+
response = session.run_powershell_script(command) do |stdout, _|
|
238
|
+
logger << stdout if stdout
|
239
|
+
end
|
222
240
|
end
|
223
241
|
|
224
242
|
[response[:exitcode], response.stderr]
|
225
243
|
end
|
226
244
|
|
245
|
+
def unelevated_temp_dir
|
246
|
+
@unelevated_temp_dir ||= session.run_powershell_script("$env:temp").stdout.chomp
|
247
|
+
end
|
248
|
+
|
227
249
|
# @return [Winrm::FileTransporter] a file transporter
|
228
250
|
# @api private
|
229
251
|
def file_transporter
|
@@ -241,6 +263,7 @@ module Kitchen
|
|
241
263
|
@connection_retries = @options.delete(:connection_retries)
|
242
264
|
@connection_retry_sleep = @options.delete(:connection_retry_sleep)
|
243
265
|
@max_wait_until_ready = @options.delete(:max_wait_until_ready)
|
266
|
+
@elevated = @options.delete(:elevated)
|
244
267
|
end
|
245
268
|
|
246
269
|
# Logs formatted standard error output at the warning level.
|
@@ -309,16 +332,33 @@ module Kitchen
|
|
309
332
|
# @return [Winrm::CommandExecutor] the command executor session
|
310
333
|
# @api private
|
311
334
|
def session(retry_options = {})
|
312
|
-
@session ||=
|
335
|
+
@session ||= service(retry_options).create_executor
|
336
|
+
end
|
337
|
+
|
338
|
+
# Creates the elevated runner for running elevated commands
|
339
|
+
#
|
340
|
+
# @return [Winrm::Elevated::Runner] the elevated runner
|
341
|
+
# @api private
|
342
|
+
def elevated_runner
|
343
|
+
@elevated_runner ||= WinRM::Elevated::Runner.new(session)
|
344
|
+
end
|
345
|
+
|
346
|
+
# Creates a winrm web service instance
|
347
|
+
#
|
348
|
+
# @param retry_options [Hash] retry options for the initial connection
|
349
|
+
# @return [Winrm::WinRMWebService] the winrm web service
|
350
|
+
# @api private
|
351
|
+
def service(retry_options = {})
|
352
|
+
@service ||= begin
|
313
353
|
opts = {
|
314
354
|
:retry_limit => connection_retries.to_i,
|
315
355
|
:retry_delay => connection_retry_sleep.to_i
|
316
356
|
}.merge(retry_options)
|
317
357
|
|
318
358
|
service_args = [endpoint, winrm_transport, options.merge(opts)]
|
319
|
-
|
320
|
-
|
321
|
-
|
359
|
+
svc = ::WinRM::WinRMWebService.new(*service_args)
|
360
|
+
svc.logger = logger
|
361
|
+
svc
|
322
362
|
end
|
323
363
|
end
|
324
364
|
|
@@ -360,6 +400,7 @@ module Kitchen
|
|
360
400
|
|
361
401
|
WINRM_SPEC_VERSION = ["~> 1.6"].freeze
|
362
402
|
WINRM_FS_SPEC_VERSION = ["~> 0.4.1"].freeze
|
403
|
+
WINRM_ELEVATED_SPEC_VERSION = ["~> 0.4.0"].freeze
|
363
404
|
|
364
405
|
# Builds the hash of options needed by the Connection object on
|
365
406
|
# construction.
|
@@ -368,6 +409,9 @@ module Kitchen
|
|
368
409
|
# @return [Hash] hash of connection options
|
369
410
|
# @api private
|
370
411
|
def connection_options(data)
|
412
|
+
elevated_password = data[:password]
|
413
|
+
elevated_password = data[:elevated_password] if data.key?(:elevated_password)
|
414
|
+
|
371
415
|
opts = {
|
372
416
|
:instance_name => instance.name,
|
373
417
|
:kitchen_root => data[:kitchen_root],
|
@@ -379,7 +423,10 @@ module Kitchen
|
|
379
423
|
:connection_retries => data[:connection_retries],
|
380
424
|
:connection_retry_sleep => data[:connection_retry_sleep],
|
381
425
|
:max_wait_until_ready => data[:max_wait_until_ready],
|
382
|
-
:winrm_transport => data[:winrm_transport]
|
426
|
+
:winrm_transport => data[:winrm_transport],
|
427
|
+
:elevated => data[:elevated],
|
428
|
+
:elevated_username => data[:elevated_username] || data[:username],
|
429
|
+
:elevated_password => elevated_password
|
383
430
|
}
|
384
431
|
opts.merge!(additional_transport_args(opts[:winrm_transport]))
|
385
432
|
opts
|
@@ -424,6 +471,7 @@ module Kitchen
|
|
424
471
|
super
|
425
472
|
load_with_rescue!("winrm", WINRM_SPEC_VERSION.dup)
|
426
473
|
load_with_rescue!("winrm-fs", WINRM_FS_SPEC_VERSION.dup)
|
474
|
+
load_with_rescue!("winrm-elevated", WINRM_ELEVATED_SPEC_VERSION.dup) if config[:elevated]
|
427
475
|
end
|
428
476
|
|
429
477
|
def load_with_rescue!(gem_name, spec_version)
|
data/lib/kitchen/version.rb
CHANGED
@@ -480,6 +480,23 @@ module Kitchen # rubocop:disable Metrics/ModuleLength
|
|
480
480
|
)
|
481
481
|
end
|
482
482
|
|
483
|
+
it "moves named_run_list into provisioner" do
|
484
|
+
DataMunger.new(
|
485
|
+
{
|
486
|
+
:provisioner => "chefy",
|
487
|
+
:suites => [
|
488
|
+
{
|
489
|
+
:name => "sweet",
|
490
|
+
:named_run_list => "other_run_list"
|
491
|
+
}
|
492
|
+
]
|
493
|
+
},
|
494
|
+
{}
|
495
|
+
).provisioner_data_for("sweet", "plat").must_equal(
|
496
|
+
:name => "chefy",
|
497
|
+
:named_run_list => "other_run_list"
|
498
|
+
)
|
499
|
+
end
|
483
500
|
it "maintains run_list in provisioner" do
|
484
501
|
DataMunger.new(
|
485
502
|
{
|
@@ -536,6 +553,23 @@ module Kitchen # rubocop:disable Metrics/ModuleLength
|
|
536
553
|
)
|
537
554
|
end
|
538
555
|
|
556
|
+
it "merge provisioner into named_run_list if provisioner exists" do
|
557
|
+
DataMunger.new(
|
558
|
+
{
|
559
|
+
:suites => [
|
560
|
+
{
|
561
|
+
:name => "sweet",
|
562
|
+
:named_run_list => "other_run_list",
|
563
|
+
:provisioner => "chefy"
|
564
|
+
}
|
565
|
+
]
|
566
|
+
},
|
567
|
+
{}
|
568
|
+
).provisioner_data_for("sweet", "plat").must_equal(
|
569
|
+
:name => "chefy",
|
570
|
+
:named_run_list => "other_run_list"
|
571
|
+
)
|
572
|
+
end
|
539
573
|
it "drops nil run_list" do
|
540
574
|
DataMunger.new(
|
541
575
|
{
|
@@ -609,6 +643,23 @@ module Kitchen # rubocop:disable Metrics/ModuleLength
|
|
609
643
|
)
|
610
644
|
end
|
611
645
|
|
646
|
+
it "moves named_run_list into provisioner" do
|
647
|
+
DataMunger.new(
|
648
|
+
{
|
649
|
+
:provisioner => "chefy",
|
650
|
+
:platforms => [
|
651
|
+
{
|
652
|
+
:name => "plat",
|
653
|
+
:named_run_list => "other_run_list"
|
654
|
+
}
|
655
|
+
]
|
656
|
+
},
|
657
|
+
{}
|
658
|
+
).provisioner_data_for("sweet", "plat").must_equal(
|
659
|
+
:name => "chefy",
|
660
|
+
:named_run_list => "other_run_list"
|
661
|
+
)
|
662
|
+
end
|
612
663
|
it "maintains run_list in provisioner" do
|
613
664
|
DataMunger.new(
|
614
665
|
{
|
@@ -665,6 +716,23 @@ module Kitchen # rubocop:disable Metrics/ModuleLength
|
|
665
716
|
)
|
666
717
|
end
|
667
718
|
|
719
|
+
it "merge provisioner into named_run_list if provisioner exists" do
|
720
|
+
DataMunger.new(
|
721
|
+
{
|
722
|
+
:platforms => [
|
723
|
+
{
|
724
|
+
:name => "plat",
|
725
|
+
:named_run_list => "other_run_list",
|
726
|
+
:provisioner => "chefy"
|
727
|
+
}
|
728
|
+
]
|
729
|
+
},
|
730
|
+
{}
|
731
|
+
).provisioner_data_for("sweet", "plat").must_equal(
|
732
|
+
:name => "chefy",
|
733
|
+
:named_run_list => "other_run_list"
|
734
|
+
)
|
735
|
+
end
|
668
736
|
it "drops nil run_list" do
|
669
737
|
DataMunger.new(
|
670
738
|
{
|
@@ -893,6 +893,170 @@ describe Kitchen::Provisioner::ChefBase do
|
|
893
893
|
end
|
894
894
|
end
|
895
895
|
|
896
|
+
describe "with a Policyfile under kitchen_root" do
|
897
|
+
|
898
|
+
let(:resolver) { stub(:resolve => true) }
|
899
|
+
|
900
|
+
describe "with the default name `Policyfile.rb`" do
|
901
|
+
before do
|
902
|
+
File.open("#{kitchen_root}/Policyfile.rb", "wb") do |file|
|
903
|
+
file.write(<<-POLICYFILE)
|
904
|
+
name 'wat'
|
905
|
+
run_list 'wat'
|
906
|
+
cookbook 'wat'
|
907
|
+
POLICYFILE
|
908
|
+
end
|
909
|
+
File.open("#{kitchen_root}/Policyfile.lock.json", "wb") do |file|
|
910
|
+
file.write(<<-POLICYFILE)
|
911
|
+
{
|
912
|
+
"name": "wat"
|
913
|
+
}
|
914
|
+
POLICYFILE
|
915
|
+
end
|
916
|
+
Kitchen::Provisioner::Chef::Policyfile.stubs(:new).returns(resolver)
|
917
|
+
end
|
918
|
+
|
919
|
+
describe "when the chef executable is not in the PATH" do
|
920
|
+
it "raises a UserError" do
|
921
|
+
Kitchen::Provisioner::Chef::Policyfile.stubs(:detect_chef_command!).with do
|
922
|
+
raise Kitchen::UserError, "Load failed"
|
923
|
+
end
|
924
|
+
proc { provisioner }.must_raise Kitchen::UserError
|
925
|
+
end
|
926
|
+
end
|
927
|
+
|
928
|
+
describe "when using a provisoner that doesn't support policyfiles" do
|
929
|
+
# This is be the default, provisioners must opt-in.
|
930
|
+
it "raises a UserError" do
|
931
|
+
proc { provisioner.create_sandbox }.must_raise Kitchen::UserError
|
932
|
+
end
|
933
|
+
end
|
934
|
+
|
935
|
+
describe "when the chef executable is in the PATH" do
|
936
|
+
|
937
|
+
before do
|
938
|
+
Kitchen::Provisioner::Chef::Policyfile.stubs(:load!)
|
939
|
+
provisioner.stubs(:supports_policyfile?).returns(true)
|
940
|
+
end
|
941
|
+
|
942
|
+
it "logs on debug that it autodetected the policyfile" do
|
943
|
+
provisioner
|
944
|
+
|
945
|
+
logged_output.string.must_match debug_line(
|
946
|
+
"Policyfile found at #{kitchen_root}/Policyfile.rb, "\
|
947
|
+
"using Policyfile to resolve dependencies")
|
948
|
+
end
|
949
|
+
|
950
|
+
it "uses uses the policyfile to resolve dependencies" do
|
951
|
+
resolver.expects(:resolve)
|
952
|
+
|
953
|
+
provisioner.create_sandbox
|
954
|
+
end
|
955
|
+
|
956
|
+
it "uses Kitchen.mutex for resolving" do
|
957
|
+
Kitchen.mutex.expects(:synchronize)
|
958
|
+
|
959
|
+
provisioner.create_sandbox
|
960
|
+
end
|
961
|
+
|
962
|
+
it "injects policyfile configuration into the dna.json" do
|
963
|
+
provisioner.create_sandbox
|
964
|
+
|
965
|
+
dna_json_file = File.join(provisioner.sandbox_path, "dna.json")
|
966
|
+
dna_json_data = JSON.parse(IO.read(dna_json_file))
|
967
|
+
|
968
|
+
expected = {
|
969
|
+
"policy_name" => "wat",
|
970
|
+
"policy_group" => "local"
|
971
|
+
}
|
972
|
+
|
973
|
+
dna_json_data.must_equal(expected)
|
974
|
+
end
|
975
|
+
end
|
976
|
+
end
|
977
|
+
describe "with a custom policyfile_path" do
|
978
|
+
|
979
|
+
let(:config) do
|
980
|
+
{
|
981
|
+
:policyfile_path => "foo-policy.rb",
|
982
|
+
:test_base_path => "/basist",
|
983
|
+
:kitchen_root => "/rooty"
|
984
|
+
}
|
985
|
+
end
|
986
|
+
|
987
|
+
before do
|
988
|
+
Kitchen::Provisioner::Chef::Policyfile.stubs(:load!)
|
989
|
+
Kitchen::Provisioner::Chef::Policyfile.stubs(:new).returns(resolver)
|
990
|
+
provisioner.stubs(:supports_policyfile?).returns(true)
|
991
|
+
end
|
992
|
+
|
993
|
+
describe "when the policyfile exists" do
|
994
|
+
|
995
|
+
let(:policyfile_path) { "#{kitchen_root}/foo-policy.rb" }
|
996
|
+
let(:policyfile_lock_path) { "#{kitchen_root}/foo-policy.lock.json" }
|
997
|
+
|
998
|
+
before do
|
999
|
+
File.open(policyfile_path, "wb") do |file|
|
1000
|
+
file.write(<<-POLICYFILE)
|
1001
|
+
name 'wat'
|
1002
|
+
run_list 'wat'
|
1003
|
+
cookbook 'wat'
|
1004
|
+
POLICYFILE
|
1005
|
+
end
|
1006
|
+
File.open(policyfile_lock_path, "wb") do |file|
|
1007
|
+
file.write(<<-POLICYFILE)
|
1008
|
+
{
|
1009
|
+
"name": "wat"
|
1010
|
+
}
|
1011
|
+
POLICYFILE
|
1012
|
+
end
|
1013
|
+
end
|
1014
|
+
|
1015
|
+
it "uses uses the policyfile to resolve dependencies" do
|
1016
|
+
Kitchen::Provisioner::Chef::Policyfile.stubs(:load!)
|
1017
|
+
resolver.expects(:resolve)
|
1018
|
+
|
1019
|
+
provisioner.create_sandbox
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
it "passes the correct path to the policyfile resolver" do
|
1023
|
+
Kitchen::Provisioner::Chef::Policyfile.
|
1024
|
+
expects(:new).
|
1025
|
+
with(policyfile_path, instance_of(String), anything).
|
1026
|
+
returns(resolver)
|
1027
|
+
|
1028
|
+
Kitchen::Provisioner::Chef::Policyfile.stubs(:load!)
|
1029
|
+
resolver.expects(:resolve)
|
1030
|
+
|
1031
|
+
provisioner.create_sandbox
|
1032
|
+
end
|
1033
|
+
end
|
1034
|
+
describe "when the policyfile doesn't exist" do
|
1035
|
+
|
1036
|
+
it "raises a UserError" do
|
1037
|
+
proc { provisioner.create_sandbox }.must_raise Kitchen::UserError
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
end
|
1041
|
+
describe "when the policyfile lock doesn't exist" do
|
1042
|
+
before do
|
1043
|
+
File.open("#{kitchen_root}/Policyfile.rb", "wb") do |file|
|
1044
|
+
file.write(<<-POLICYFILE)
|
1045
|
+
name 'wat'
|
1046
|
+
run_list 'wat'
|
1047
|
+
cookbook 'wat'
|
1048
|
+
POLICYFILE
|
1049
|
+
end
|
1050
|
+
|
1051
|
+
it "runs `chef install` to generate the lock" do
|
1052
|
+
resolver.expects(:compile)
|
1053
|
+
provisioner.create_sandbox
|
1054
|
+
end
|
1055
|
+
end
|
1056
|
+
end
|
1057
|
+
end
|
1058
|
+
end
|
1059
|
+
|
896
1060
|
describe "with a Berksfile under kitchen_root" do
|
897
1061
|
|
898
1062
|
let(:resolver) { stub(:resolve => true) }
|
@@ -21,6 +21,7 @@ require_relative "../../spec_helper"
|
|
21
21
|
require "kitchen/transport/winrm"
|
22
22
|
require "winrm"
|
23
23
|
require "winrm-fs"
|
24
|
+
require "winrm-elevated"
|
24
25
|
|
25
26
|
module Kitchen
|
26
27
|
|
@@ -108,6 +109,10 @@ describe Kitchen::Transport::Winrm do
|
|
108
109
|
it "sets :winrm_transport to :negotiate" do
|
109
110
|
transport[:winrm_transport].must_equal :negotiate
|
110
111
|
end
|
112
|
+
|
113
|
+
it "sets :elevated to false" do
|
114
|
+
transport[:elevated].must_equal false
|
115
|
+
end
|
111
116
|
end
|
112
117
|
|
113
118
|
describe "#connection" do
|
@@ -326,6 +331,59 @@ describe Kitchen::Transport::Winrm do
|
|
326
331
|
make_connection
|
327
332
|
end
|
328
333
|
|
334
|
+
it "sets elevated_username from user by default" do
|
335
|
+
config[:username] = "user"
|
336
|
+
|
337
|
+
klass.expects(:new).with do |hash|
|
338
|
+
hash[:elevated_username] == "user"
|
339
|
+
end
|
340
|
+
|
341
|
+
make_connection
|
342
|
+
end
|
343
|
+
|
344
|
+
it "sets elevated_username from overriden elevated_username" do
|
345
|
+
config[:username] = "user"
|
346
|
+
config[:elevated_username] = "elevated_user"
|
347
|
+
|
348
|
+
klass.expects(:new).with do |hash|
|
349
|
+
hash[:elevated_username] == "elevated_user"
|
350
|
+
end
|
351
|
+
|
352
|
+
make_connection
|
353
|
+
end
|
354
|
+
|
355
|
+
it "sets elevated_password from user by default" do
|
356
|
+
config[:password] = "pass"
|
357
|
+
|
358
|
+
klass.expects(:new).with do |hash|
|
359
|
+
hash[:elevated_password] == "pass"
|
360
|
+
end
|
361
|
+
|
362
|
+
make_connection
|
363
|
+
end
|
364
|
+
|
365
|
+
it "sets elevated_password from overriden elevated_password" do
|
366
|
+
config[:password] = "pass"
|
367
|
+
config[:elevated_password] = "elevated_pass"
|
368
|
+
|
369
|
+
klass.expects(:new).with do |hash|
|
370
|
+
hash[:elevated_password] == "elevated_pass"
|
371
|
+
end
|
372
|
+
|
373
|
+
make_connection
|
374
|
+
end
|
375
|
+
|
376
|
+
it "sets elevated_password to nil if overriden elevated_password is nil" do
|
377
|
+
config[:password] = "pass"
|
378
|
+
config[:elevated_password] = nil
|
379
|
+
|
380
|
+
klass.expects(:new).with do |hash|
|
381
|
+
hash[:elevated_password].nil?
|
382
|
+
end
|
383
|
+
|
384
|
+
make_connection
|
385
|
+
end
|
386
|
+
|
329
387
|
describe "when negotiate is set in config" do
|
330
388
|
before do
|
331
389
|
config[:winrm_transport] = "negotiate"
|
@@ -406,6 +464,31 @@ describe Kitchen::Transport::Winrm do
|
|
406
464
|
end
|
407
465
|
|
408
466
|
describe "#load_needed_dependencies" do
|
467
|
+
describe "winrm-elevated" do
|
468
|
+
let(:transport) { Kitchen::Transport::Winrm.new(config) }
|
469
|
+
|
470
|
+
before do
|
471
|
+
transport.stubs(:require).with("winrm")
|
472
|
+
transport.stubs(:require).with("winrm-fs")
|
473
|
+
end
|
474
|
+
|
475
|
+
describe "elevated is false" do
|
476
|
+
it "does not require winrm-elevated" do
|
477
|
+
transport.expects(:require).with("winrm-elevated").never
|
478
|
+
transport.finalize_config!(instance)
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
describe "elevated is true" do
|
483
|
+
before { config[:elevated] = true }
|
484
|
+
|
485
|
+
it "does requires winrm-elevated" do
|
486
|
+
transport.expects(:require).with("winrm-elevated")
|
487
|
+
transport.finalize_config!(instance)
|
488
|
+
end
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
409
492
|
describe "winrm-fs" do
|
410
493
|
before do
|
411
494
|
# force loading of winrm-fs to get the version constant
|
@@ -656,6 +739,76 @@ describe Kitchen::Transport::Winrm::Connection do
|
|
656
739
|
end
|
657
740
|
end
|
658
741
|
|
742
|
+
describe "elevated command" do
|
743
|
+
let(:response) do
|
744
|
+
o = WinRM::Output.new
|
745
|
+
o[:exitcode] = 0
|
746
|
+
o[:data].concat([
|
747
|
+
{ :stdout => "ok\r\n" },
|
748
|
+
{ :stderr => "congrats\r\n" }
|
749
|
+
])
|
750
|
+
o
|
751
|
+
end
|
752
|
+
let(:env_temp_response) do
|
753
|
+
o = WinRM::Output.new
|
754
|
+
o[:exitcode] = 0
|
755
|
+
o[:data].concat([
|
756
|
+
{ :stdout => "temp_dir" }
|
757
|
+
])
|
758
|
+
o
|
759
|
+
end
|
760
|
+
let(:elevated_runner) do
|
761
|
+
r = mock("elevated_runner")
|
762
|
+
r.responds_like_instance_of(WinRM::Elevated::Runner)
|
763
|
+
r
|
764
|
+
end
|
765
|
+
|
766
|
+
before do
|
767
|
+
options[:elevated] = true
|
768
|
+
WinRM::Elevated::Runner.stubs(:new).with(executor).returns(elevated_runner)
|
769
|
+
end
|
770
|
+
|
771
|
+
describe "elevated user is not login user" do
|
772
|
+
before do
|
773
|
+
options[:elevated_username] = "username"
|
774
|
+
options[:elevated_password] = "password"
|
775
|
+
executor.expects(:run_powershell_script).
|
776
|
+
with("$env:temp").returns(env_temp_response)
|
777
|
+
elevated_runner.expects(:powershell_elevated).
|
778
|
+
with(
|
779
|
+
"$env:temp='temp_dir';doit",
|
780
|
+
options[:elevated_username],
|
781
|
+
options[:elevated_password]
|
782
|
+
).yields("ok\n", nil).returns(response)
|
783
|
+
end
|
784
|
+
|
785
|
+
it "logger captures stdout" do
|
786
|
+
connection.execute("doit")
|
787
|
+
|
788
|
+
logged_output.string.must_match(/^ok$/)
|
789
|
+
end
|
790
|
+
end
|
791
|
+
|
792
|
+
describe "elevator user is login user" do
|
793
|
+
before do
|
794
|
+
options[:elevated_username] = options[:user]
|
795
|
+
options[:elevated_password] = options[:pass]
|
796
|
+
elevated_runner.expects(:powershell_elevated).
|
797
|
+
with(
|
798
|
+
"doit",
|
799
|
+
options[:elevated_username],
|
800
|
+
options[:elevated_password]
|
801
|
+
).yields("ok\n", nil).returns(response)
|
802
|
+
end
|
803
|
+
|
804
|
+
it "logger captures stdout" do
|
805
|
+
connection.execute("doit")
|
806
|
+
|
807
|
+
logged_output.string.must_match(/^ok$/)
|
808
|
+
end
|
809
|
+
end
|
810
|
+
end
|
811
|
+
|
659
812
|
describe "long command" do
|
660
813
|
let(:command) { %{Write-Host "#{"a" * 4000}"} }
|
661
814
|
|
data/test-kitchen.gemspec
CHANGED
@@ -34,6 +34,7 @@ Gem::Specification.new do |gem|
|
|
34
34
|
gem.add_development_dependency "pry-byebug"
|
35
35
|
gem.add_development_dependency "pry-stack_explorer"
|
36
36
|
gem.add_development_dependency "winrm", "~> 1.6"
|
37
|
+
gem.add_development_dependency "winrm-elevated", "~> 0.4.0"
|
37
38
|
gem.add_development_dependency "winrm-fs", "~> 0.4.1"
|
38
39
|
|
39
40
|
gem.add_development_dependency "bundler", "~> 1.3"
|
data/testing_windows.md
CHANGED
@@ -9,6 +9,7 @@ Ensure that the cookbook's root directory includes a `Gemfile` that includes you
|
|
9
9
|
gem 'test-kitchen', git: 'https://github.com/mwrock/test-kitchen', branch: 'winrm-fs'
|
10
10
|
gem 'winrm', '~> 1.6'
|
11
11
|
gem 'winrm-fs', '~> 0.4.1'
|
12
|
+
gem 'winrm-elevated', '~> 0.4.0'
|
12
13
|
```
|
13
14
|
The above would target the `winrm-fs` branch in mwrock's test-kitchen repo.
|
14
15
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: test-kitchen
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Fletcher Nichol
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-05-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mixlib-shellout
|
@@ -168,6 +168,20 @@ dependencies:
|
|
168
168
|
- - "~>"
|
169
169
|
- !ruby/object:Gem::Version
|
170
170
|
version: '1.6'
|
171
|
+
- !ruby/object:Gem::Dependency
|
172
|
+
name: winrm-elevated
|
173
|
+
requirement: !ruby/object:Gem::Requirement
|
174
|
+
requirements:
|
175
|
+
- - "~>"
|
176
|
+
- !ruby/object:Gem::Version
|
177
|
+
version: 0.4.0
|
178
|
+
type: :development
|
179
|
+
prerelease: false
|
180
|
+
version_requirements: !ruby/object:Gem::Requirement
|
181
|
+
requirements:
|
182
|
+
- - "~>"
|
183
|
+
- !ruby/object:Gem::Version
|
184
|
+
version: 0.4.0
|
171
185
|
- !ruby/object:Gem::Dependency
|
172
186
|
name: winrm-fs
|
173
187
|
requirement: !ruby/object:Gem::Requirement
|
@@ -465,6 +479,7 @@ files:
|
|
465
479
|
- lib/kitchen/provisioner/chef/berkshelf.rb
|
466
480
|
- lib/kitchen/provisioner/chef/common_sandbox.rb
|
467
481
|
- lib/kitchen/provisioner/chef/librarian.rb
|
482
|
+
- lib/kitchen/provisioner/chef/policyfile.rb
|
468
483
|
- lib/kitchen/provisioner/chef_apply.rb
|
469
484
|
- lib/kitchen/provisioner/chef_base.rb
|
470
485
|
- lib/kitchen/provisioner/chef_solo.rb
|
@@ -587,7 +602,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
587
602
|
version: '0'
|
588
603
|
requirements: []
|
589
604
|
rubyforge_project:
|
590
|
-
rubygems_version: 2.
|
605
|
+
rubygems_version: 2.6.3
|
591
606
|
signing_key:
|
592
607
|
specification_version: 4
|
593
608
|
summary: Test Kitchen is an integration tool for developing and testing infrastructure
|