test-kitchen 1.2.1 → 1.3.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 (114) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +1 -1
  3. data/.rubocop.yml +3 -0
  4. data/.travis.yml +20 -9
  5. data/CHANGELOG.md +219 -108
  6. data/Gemfile +10 -6
  7. data/Guardfile +38 -9
  8. data/README.md +11 -1
  9. data/Rakefile +21 -37
  10. data/bin/kitchen +4 -4
  11. data/features/kitchen_action_commands.feature +161 -0
  12. data/features/kitchen_console_command.feature +34 -0
  13. data/features/kitchen_diagnose_command.feature +64 -0
  14. data/features/kitchen_init_command.feature +29 -17
  15. data/features/kitchen_list_command.feature +2 -2
  16. data/features/kitchen_login_command.feature +56 -0
  17. data/features/{sink_command.feature → kitchen_sink_command.feature} +0 -0
  18. data/features/kitchen_test_command.feature +88 -0
  19. data/features/step_definitions/gem_steps.rb +8 -6
  20. data/features/step_definitions/git_steps.rb +4 -2
  21. data/features/step_definitions/output_steps.rb +5 -0
  22. data/features/support/env.rb +12 -9
  23. data/lib/kitchen.rb +60 -38
  24. data/lib/kitchen/base64_stream.rb +55 -0
  25. data/lib/kitchen/busser.rb +124 -58
  26. data/lib/kitchen/cli.rb +121 -38
  27. data/lib/kitchen/collection.rb +3 -3
  28. data/lib/kitchen/color.rb +4 -4
  29. data/lib/kitchen/command.rb +78 -11
  30. data/lib/kitchen/command/action.rb +3 -2
  31. data/lib/kitchen/command/console.rb +12 -5
  32. data/lib/kitchen/command/diagnose.rb +17 -3
  33. data/lib/kitchen/command/driver_discover.rb +26 -7
  34. data/lib/kitchen/command/exec.rb +41 -0
  35. data/lib/kitchen/command/list.rb +44 -14
  36. data/lib/kitchen/command/login.rb +2 -1
  37. data/lib/kitchen/command/sink.rb +2 -1
  38. data/lib/kitchen/command/test.rb +5 -4
  39. data/lib/kitchen/config.rb +146 -14
  40. data/lib/kitchen/configurable.rb +314 -0
  41. data/lib/kitchen/data_munger.rb +522 -18
  42. data/lib/kitchen/diagnostic.rb +43 -4
  43. data/lib/kitchen/driver.rb +4 -4
  44. data/lib/kitchen/driver/base.rb +80 -115
  45. data/lib/kitchen/driver/dummy.rb +34 -6
  46. data/lib/kitchen/driver/proxy.rb +14 -3
  47. data/lib/kitchen/driver/ssh_base.rb +61 -7
  48. data/lib/kitchen/errors.rb +109 -9
  49. data/lib/kitchen/generator/driver_create.rb +39 -5
  50. data/lib/kitchen/generator/init.rb +130 -45
  51. data/lib/kitchen/instance.rb +162 -28
  52. data/lib/kitchen/lazy_hash.rb +79 -7
  53. data/lib/kitchen/loader/yaml.rb +159 -27
  54. data/lib/kitchen/logger.rb +267 -21
  55. data/lib/kitchen/logging.rb +30 -3
  56. data/lib/kitchen/login_command.rb +11 -2
  57. data/lib/kitchen/metadata_chopper.rb +2 -2
  58. data/lib/kitchen/provisioner.rb +4 -4
  59. data/lib/kitchen/provisioner/base.rb +107 -103
  60. data/lib/kitchen/provisioner/chef/berkshelf.rb +36 -8
  61. data/lib/kitchen/provisioner/chef/librarian.rb +40 -11
  62. data/lib/kitchen/provisioner/chef_base.rb +206 -167
  63. data/lib/kitchen/provisioner/chef_solo.rb +25 -7
  64. data/lib/kitchen/provisioner/chef_zero.rb +105 -29
  65. data/lib/kitchen/provisioner/dummy.rb +1 -1
  66. data/lib/kitchen/provisioner/shell.rb +21 -6
  67. data/lib/kitchen/rake_tasks.rb +8 -3
  68. data/lib/kitchen/shell_out.rb +15 -18
  69. data/lib/kitchen/ssh.rb +122 -27
  70. data/lib/kitchen/state_file.rb +24 -7
  71. data/lib/kitchen/thor_tasks.rb +9 -4
  72. data/lib/kitchen/util.rb +43 -118
  73. data/lib/kitchen/version.rb +1 -1
  74. data/lib/vendor/hash_recursive_merge.rb +10 -2
  75. data/spec/kitchen/base64_stream_spec.rb +77 -0
  76. data/spec/kitchen/busser_spec.rb +490 -0
  77. data/spec/kitchen/collection_spec.rb +10 -10
  78. data/spec/kitchen/color_spec.rb +2 -2
  79. data/spec/kitchen/config_spec.rb +234 -62
  80. data/spec/kitchen/configurable_spec.rb +490 -0
  81. data/spec/kitchen/data_munger_spec.rb +1070 -862
  82. data/spec/kitchen/diagnostic_spec.rb +79 -0
  83. data/spec/kitchen/driver/base_spec.rb +80 -85
  84. data/spec/kitchen/driver/dummy_spec.rb +43 -14
  85. data/spec/kitchen/driver/proxy_spec.rb +134 -0
  86. data/spec/kitchen/driver/ssh_base_spec.rb +644 -0
  87. data/spec/kitchen/driver_spec.rb +15 -15
  88. data/spec/kitchen/errors_spec.rb +309 -0
  89. data/spec/kitchen/instance_spec.rb +143 -46
  90. data/spec/kitchen/lazy_hash_spec.rb +36 -9
  91. data/spec/kitchen/loader/yaml_spec.rb +237 -226
  92. data/spec/kitchen/logger_spec.rb +419 -0
  93. data/spec/kitchen/logging_spec.rb +59 -0
  94. data/spec/kitchen/login_command_spec.rb +49 -0
  95. data/spec/kitchen/metadata_chopper_spec.rb +82 -0
  96. data/spec/kitchen/platform_spec.rb +4 -4
  97. data/spec/kitchen/provisioner/base_spec.rb +65 -125
  98. data/spec/kitchen/provisioner/chef_base_spec.rb +798 -0
  99. data/spec/kitchen/provisioner/chef_solo_spec.rb +316 -0
  100. data/spec/kitchen/provisioner/chef_zero_spec.rb +624 -0
  101. data/spec/kitchen/provisioner/shell_spec.rb +269 -0
  102. data/spec/kitchen/provisioner_spec.rb +6 -6
  103. data/spec/kitchen/shell_out_spec.rb +143 -0
  104. data/spec/kitchen/ssh_spec.rb +683 -0
  105. data/spec/kitchen/state_file_spec.rb +28 -21
  106. data/spec/kitchen/suite_spec.rb +7 -7
  107. data/spec/kitchen/util_spec.rb +68 -10
  108. data/spec/kitchen_spec.rb +107 -0
  109. data/spec/spec_helper.rb +18 -13
  110. data/support/chef-client-zero.rb +10 -9
  111. data/support/chef_helpers.sh +16 -0
  112. data/support/download_helpers.sh +109 -0
  113. data/test-kitchen.gemspec +42 -33
  114. metadata +107 -33
@@ -0,0 +1,55 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2014, 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
+ module Kitchen
20
+
21
+ # Base64 encoder/decoder that operates on IO objects so as to minimize
22
+ # memory allocations on large payloads.
23
+ #
24
+ # @author Fletcher Nichol <fnichol@nichol.ca>
25
+ module Base64Stream
26
+
27
+ # Encodes an input stream into a Base64 output stream. The input and ouput
28
+ # objects must be opened IO resources. In other words, opening and closing
29
+ # the resources are not the responsibilty of this method.
30
+ #
31
+ # @param io_in [#read] input stream
32
+ # @param io_out [#write] output stream
33
+ def self.strict_encode(io_in, io_out)
34
+ buffer = "" # rubocop:disable Lint/UselessAssignment
35
+ while io_in.read(3 * 1000, buffer)
36
+ io_out.write([buffer].pack("m0"))
37
+ end
38
+ buffer = nil # rubocop:disable Lint/UselessAssignment
39
+ end
40
+
41
+ # Decodes a Base64 input stream into an output stream. The input and ouput
42
+ # objects must be opened IO resources. In other words, opening and closing
43
+ # the resources are not the responsibilty of this method.
44
+ #
45
+ # @param io_in [#read] input stream
46
+ # @param io_out [#write] output stream
47
+ def self.strict_decode(io_in, io_out)
48
+ buffer = "" # rubocop:disable Lint/UselessAssignment
49
+ while io_in.read(3 * 1000, buffer)
50
+ io_out.write(buffer.unpack("m0").first)
51
+ end
52
+ buffer = nil # rubocop:disable Lint/UselessAssignment
53
+ end
54
+ end
55
+ end
@@ -16,8 +16,8 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
- require 'base64'
20
- require 'digest'
19
+ require "base64"
20
+ require "digest"
21
21
 
22
22
  module Kitchen
23
23
 
@@ -95,21 +95,23 @@ module Kitchen
95
95
  # @return [String] a command string to setup the test suite, or nil if no
96
96
  # work needs to be performed
97
97
  def setup_cmd
98
- @setup_cmd ||= if local_suite_files.empty?
99
- nil
100
- else
101
- setup_cmd = []
102
- setup_cmd << busser_setup_env
103
- setup_cmd << "if ! #{sudo}#{config[:ruby_bindir]}/gem list busser -i >/dev/null"
104
- setup_cmd << "then #{sudo}#{config[:ruby_bindir]}/gem install #{gem_install_args}"
105
- setup_cmd << "fi"
106
- setup_cmd << "gem_bindir=`#{config[:ruby_bindir]}/ruby -rrubygems -e \"puts Gem.bindir\"`"
107
- setup_cmd << "#{sudo}${gem_bindir}/busser setup"
108
- setup_cmd << "#{sudo}#{config[:busser_bin]} plugin install #{plugins.join(' ')}"
109
-
110
- # use Bourne (/bin/sh) as Bash does not exist on all Unix flavors
111
- "sh -c '#{setup_cmd.join('; ')}'"
112
- end
98
+ return if local_suite_files.empty?
99
+
100
+ ruby = "#{config[:ruby_bindir]}/ruby"
101
+ gem = sudo("#{config[:ruby_bindir]}/gem")
102
+ busser = sudo(config[:busser_bin])
103
+
104
+ cmd = <<-CMD.gsub(/^ {8}/, "")
105
+ #{busser_setup_env}
106
+ gem_bindir=`#{ruby} -rrubygems -e "puts Gem.bindir"`
107
+
108
+ if ! #{gem} list busser -i >/dev/null; then
109
+ #{gem} install #{gem_install_args}
110
+ fi
111
+ #{sudo("${gem_bindir}")}/busser setup
112
+ #{busser} plugin install #{plugins.join(" ")}
113
+ CMD
114
+ Util.wrap_command(cmd)
113
115
  end
114
116
 
115
117
  # Returns a command string which transfers all suite test files to the
@@ -121,18 +123,22 @@ module Kitchen
121
123
  # @return [String] a command string to transfer all suite test files, or
122
124
  # nil if no work needs to be performed.
123
125
  def sync_cmd
124
- @sync_cmd ||= if local_suite_files.empty?
125
- nil
126
- else
127
- sync_cmd = []
128
- sync_cmd << busser_setup_env
129
- sync_cmd << "#{sudo}#{config[:busser_bin]} suite cleanup"
130
- sync_cmd << "#{local_suite_files.map { |f| stream_file(f, remote_file(f, config[:suite_name])) }.join("; ")}"
131
- sync_cmd << "#{helper_files.map { |f| stream_file(f, remote_file(f, "helpers")) }.join("; ")}"
132
-
133
- # use Bourne (/bin/sh) as Bash does not exist on all Unix flavors
134
- "sh -c '#{sync_cmd.join('; ')}'"
126
+ return if local_suite_files.empty?
127
+
128
+ cmd = <<-CMD.gsub(/^ {8}/, "")
129
+ #{busser_setup_env}
130
+
131
+ #{sudo(config[:busser_bin])} suite cleanup
132
+
133
+ CMD
134
+ local_suite_files.each do |f|
135
+ cmd << stream_file(f, remote_file(f, config[:suite_name])).concat("\n")
136
+ end
137
+ helper_files.each do |f|
138
+ cmd << stream_file(f, remote_file(f, "helpers")).concat("\n")
135
139
  end
140
+
141
+ Util.wrap_command(cmd)
136
142
  end
137
143
 
138
144
  # Returns a command string which runs all Busser suite tests for the suite.
@@ -143,16 +149,15 @@ module Kitchen
143
149
  # @return [String] a command string to run the test suites, or nil if no
144
150
  # work needs to be performed
145
151
  def run_cmd
146
- @run_cmd ||= if local_suite_files.empty?
147
- nil
148
- else
149
- run_cmd = []
150
- run_cmd << busser_setup_env
151
- run_cmd << "#{sudo}#{config[:busser_bin]} test"
152
-
153
- # use Bourne (/bin/sh) as Bash does not exist on all Unix flavors
154
- "sh -c '#{run_cmd.join('; ')}'"
155
- end
152
+ return if local_suite_files.empty?
153
+
154
+ cmd = <<-CMD.gsub(/^ {8}/, "")
155
+ #{busser_setup_env}
156
+
157
+ #{sudo(config[:busser_bin])} test
158
+ CMD
159
+
160
+ Util.wrap_command(cmd)
156
161
  end
157
162
 
158
163
  private
@@ -160,83 +165,144 @@ module Kitchen
160
165
  DEFAULT_RUBY_BINDIR = "/opt/chef/embedded/bin".freeze
161
166
  DEFAULT_ROOT_PATH = "/tmp/busser".freeze
162
167
 
168
+ # @return [Hash] a configuration hash
169
+ # @api private
163
170
  attr_reader :config
164
171
 
172
+ # Ensures that the object is internally consistent and otherwise raising
173
+ # an exception.
174
+ #
175
+ # @param suite_name [String] the suite name
176
+ # @raise [ClientError] if a suite name is missing
177
+ # @raise [UserError] if the suite name is invalid
178
+ # @api private
165
179
  def validate_options(suite_name)
166
180
  if suite_name.nil?
167
181
  raise ClientError, "Busser#new requires a suite_name"
168
182
  end
169
183
 
170
- if suite_name == 'helper'
184
+ if suite_name == "helper"
171
185
  raise UserError,
172
186
  "Suite name invalid: 'helper' is a reserved directory name."
173
187
  end
174
188
  end
175
189
 
190
+ # Returns a uniquely sorted Array of Busser plugin gems that need to
191
+ # be installed for the related suite.
192
+ #
193
+ # @return [Array<String>] a lexically sorted, unique item array of Busser
194
+ # plugin gem names
195
+ # @api private
176
196
  def plugins
197
+ non_suite_dirs = %w[data data_bags environments nodes roles]
177
198
  glob = File.join(config[:test_base_path], config[:suite_name], "*")
178
199
  Dir.glob(glob).reject { |d|
179
- ! File.directory?(d) || non_suite_dirs.include?(File.basename(d))
200
+ !File.directory?(d) || non_suite_dirs.include?(File.basename(d))
180
201
  }.map { |d| "busser-#{File.basename(d)}" }.sort.uniq
181
202
  end
182
203
 
204
+ # Returns an Array of test suite filenames for the related suite currently
205
+ # residing on the local workstation. Any special provisioner-specific
206
+ # directories (such as a Chef roles/ directory) are excluded.
207
+ #
208
+ # @return [Array<String>] array of suite files
209
+ # @api private
183
210
  def local_suite_files
184
211
  base = File.join(config[:test_base_path], config[:suite_name])
185
212
  glob = File.join(base, "*/**/*")
186
213
  Dir.glob(glob).reject do |f|
187
- is_chef_data_dir?(base, f) || File.directory?(f)
214
+ chef_data_dir?(base, f) || File.directory?(f)
188
215
  end
189
216
  end
190
217
 
191
- def is_chef_data_dir?(base, file)
192
- file =~ %r[^#{base}/(data|data_bags|environments|nodes|roles)/]
218
+ # Determines whether or not a local workstation file exists under a
219
+ # Chef-related directory.
220
+ #
221
+ # @return [truthy,falsey] whether or not a given file is some kind of
222
+ # Chef-related file
223
+ # @api private
224
+ def chef_data_dir?(base, file)
225
+ file =~ %r{^#{base}/(data|data_bags|environments|nodes|roles)/}
193
226
  end
194
227
 
228
+ # Returns an Array of common helper filenames currently residing on the
229
+ # local workstation.
230
+ #
231
+ # @return [Array<String>] array of helper files
232
+ # @api private
195
233
  def helper_files
196
- Dir.glob(File.join(config[:test_base_path], "helpers", "*/**/*")).reject { |f| File.directory?(f) }
234
+ glob = File.join(config[:test_base_path], "helpers", "*/**/*")
235
+ Dir.glob(glob).reject { |f| File.directory?(f) }
197
236
  end
198
237
 
238
+ # Returns a command string that will, once evaluated, result in the
239
+ # fully qualified destination path of a file on an instance.
240
+ #
241
+ # @param file [String] absolute path to the local file
242
+ # @param dir [String] suite directory or helper directory name
243
+ # @return [String] command string
244
+ # @api private
199
245
  def remote_file(file, dir)
200
246
  local_prefix = File.join(config[:test_base_path], dir)
201
- "$(#{sudo}#{config[:busser_bin]} suite path)/".concat(file.sub(%r{^#{local_prefix}/}, ''))
247
+ "`#{sudo(config[:busser_bin])} suite path`/".
248
+ concat(file.sub(%r{^#{local_prefix}/}, ""))
202
249
  end
203
250
 
251
+ # Returns a command string that will, once evaluated, result in the copying
252
+ # of a local file to a remote instance.
253
+ #
254
+ # @param local_path [String] the path to a local source file for copying
255
+ # @param remote_path [String] the destrination path on the remote instance
256
+ # @return [String] command string
257
+ # @api private
204
258
  def stream_file(local_path, remote_path)
205
259
  local_file = IO.read(local_path)
260
+ encoded_file = Base64.encode64(local_file).gsub("\n", "")
206
261
  md5 = Digest::MD5.hexdigest(local_file)
207
- perms = sprintf("%o", File.stat(local_path).mode)[2, 4]
262
+ perms = format("%o", File.stat(local_path).mode)[2, 4]
208
263
  stream_cmd = [
209
- "#{sudo}#{config[:busser_bin]}",
264
+ sudo(config[:busser_bin]),
210
265
  "deserialize",
211
266
  "--destination=#{remote_path}",
212
267
  "--md5sum=#{md5}",
213
268
  "--perms=#{perms}"
214
269
  ].join(" ")
215
270
 
216
- stream_file_cmd = []
217
- stream_file_cmd << %{echo "Uploading #{remote_path} (mode=#{perms})"}
218
- stream_file_cmd << %{echo "#{Base64.encode64(local_file).gsub("\n", '')}" | #{sudo}#{stream_cmd}}
219
- stream_file_cmd.join('; ')
220
- end
221
-
222
- def sudo
223
- config[:sudo] ? "sudo -E " : ""
271
+ [
272
+ %{echo "Uploading #{remote_path} (mode=#{perms})"},
273
+ %{echo "#{encoded_file}" | #{stream_cmd}}
274
+ ].join("\n").concat("\n")
224
275
  end
225
276
 
226
- def non_suite_dirs
227
- %w{data data_bags environments nodes roles}
277
+ # Conditionally prefixes a command with a sudo command.
278
+ #
279
+ # @param command [String] command to be prefixed
280
+ # @return [String] the command, conditionaly prefixed with sudo
281
+ # @api private
282
+ def sudo(command)
283
+ config[:sudo] ? "sudo -E #{command}" : command
228
284
  end
229
285
 
286
+ # Returns a command string that sets appropriate environment variables for
287
+ # busser commands.
288
+ #
289
+ # @return [String] command string
290
+ # @api private
230
291
  def busser_setup_env
231
292
  [
232
293
  %{BUSSER_ROOT="#{config[:root_path]}"},
233
294
  %{GEM_HOME="#{config[:root_path]}/gems"},
234
295
  %{GEM_PATH="#{config[:root_path]}/gems"},
235
296
  %{GEM_CACHE="#{config[:root_path]}/gems/cache"},
236
- %{; export BUSSER_ROOT GEM_HOME GEM_PATH GEM_CACHE}
297
+ %{\nexport BUSSER_ROOT GEM_HOME GEM_PATH GEM_CACHE}
237
298
  ].join(" ")
238
299
  end
239
300
 
301
+ # Returns arguments to a `gem install` command, suitable to install the
302
+ # Busser gem.
303
+ #
304
+ # @return [String] arguments string
305
+ # @api private
240
306
  def gem_install_args
241
307
  gem, version = config[:version].split("@")
242
308
  gem, version = "busser", gem if gem =~ /^\d+\.\d+\.\d+/
@@ -16,11 +16,11 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
- require 'thor'
19
+ require "thor"
20
20
 
21
- require 'kitchen'
22
- require 'kitchen/generator/driver_create'
23
- require 'kitchen/generator/init'
21
+ require "kitchen"
22
+ require "kitchen/generator/driver_create"
23
+ require "kitchen/generator/init"
24
24
 
25
25
  module Kitchen
26
26
 
@@ -32,12 +32,21 @@ module Kitchen
32
32
  # Common module to load and invoke a CLI-implementation agnostic command.
33
33
  module PerformCommand
34
34
 
35
+ # Perform a CLI subcommand.
36
+ #
37
+ # @param task [String] action to take, usually corresponding to the
38
+ # subcommand name
39
+ # @param command [String] command class to create and invoke]
40
+ # @param args [Array] remainder arguments from processed ARGV
41
+ # (default: `nil`)
42
+ # @param additional_options [Hash] additional configuration needed to
43
+ # set up the command class (default: `{}`)
35
44
  def perform(task, command, args = nil, additional_options = {})
36
45
  require "kitchen/command/#{command}"
37
46
 
38
47
  command_options = {
39
48
  :action => task,
40
- :help => lambda { help(task) },
49
+ :help => -> { help(task) },
41
50
  :config => @config,
42
51
  :shell => shell
43
52
  }.merge(additional_options)
@@ -51,6 +60,8 @@ module Kitchen
51
60
  include Logging
52
61
  include PerformCommand
53
62
 
63
+ # The maximum number of concurrent instances that can run--which is a bit
64
+ # high
54
65
  MAX_CONCURRENCY = 9999
55
66
 
56
67
  # Constructs a new instance.
@@ -59,22 +70,27 @@ module Kitchen
59
70
  $stdout.sync = true
60
71
  Kitchen.logger = Kitchen.default_file_logger
61
72
  @loader = Kitchen::Loader::YAML.new(
62
- :project_config => ENV['KITCHEN_YAML'],
63
- :local_config => ENV['KITCHEN_LOCAL_YAML'],
64
- :global_config => ENV['KITCHEN_GLOBAL_YAML']
73
+ :project_config => ENV["KITCHEN_YAML"],
74
+ :local_config => ENV["KITCHEN_LOCAL_YAML"],
75
+ :global_config => ENV["KITCHEN_GLOBAL_YAML"]
65
76
  )
66
77
  @config = Kitchen::Config.new(
67
78
  :loader => @loader,
68
- :log_level => ENV.fetch('KITCHEN_LOG', "info").downcase.to_sym
79
+ :log_level => ENV.fetch("KITCHEN_LOG", "info").downcase.to_sym
69
80
  )
70
81
  end
71
82
 
72
83
  desc "list [INSTANCE|REGEXP|all]", "Lists one or more instances"
73
- method_option :bare, :aliases => "-b", :type => :boolean,
84
+ method_option :bare,
85
+ :aliases => "-b",
86
+ :type => :boolean,
74
87
  :desc => "List the name of each instance only, one per line"
75
- method_option :debug, :aliases => "-d", :type => :boolean,
88
+ method_option :debug,
89
+ :aliases => "-d",
90
+ :type => :boolean,
76
91
  :desc => "[Deprecated] Please use `kitchen diagnose'"
77
- method_option :log_level, :aliases => "-l",
92
+ method_option :log_level,
93
+ :aliases => "-l",
78
94
  :desc => "Set the log level (debug, info, warn, error, fatal)"
79
95
  def list(*args)
80
96
  update_config!
@@ -82,36 +98,63 @@ module Kitchen
82
98
  end
83
99
 
84
100
  desc "diagnose [INSTANCE|REGEXP|all]", "Show computed diagnostic configuration"
85
- method_option :log_level, :aliases => "-l",
101
+ method_option :log_level,
102
+ :aliases => "-l",
86
103
  :desc => "Set the log level (debug, info, warn, error, fatal)"
87
- method_option :loader, :type => :boolean,
104
+ method_option :loader,
105
+ :type => :boolean,
88
106
  :desc => "Include data loader diagnostics"
89
- method_option :instances, :type => :boolean, :default => true,
107
+ method_option :instances,
108
+ :type => :boolean,
109
+ :default => true,
90
110
  :desc => "Include instances diagnostics"
91
- method_option :all, :type => :boolean,
111
+ method_option :all,
112
+ :type => :boolean,
92
113
  :desc => "Include all diagnostics"
93
114
  def diagnose(*args)
94
115
  update_config!
95
116
  perform("diagnose", "diagnose", args, :loader => @loader)
96
117
  end
97
118
 
98
- [:create, :converge, :setup, :verify, :destroy].each do |action|
119
+ {
120
+ :create => "Change instance state to create. " \
121
+ "Start one or more instances",
122
+ :converge => "Change instance state to converge. " \
123
+ "Use a provisioner to configure one or more instances",
124
+ :setup => "Change instance state to setup. " \
125
+ "Prepare to run automated tests. " \
126
+ "Install busser and related gems on one or more instances",
127
+ :verify => "Change instance state to verify. " \
128
+ "Run automated tests on one or more instances",
129
+ :destroy => "Change instance state to destroy. " \
130
+ "Delete all information for one or more instances"
131
+ }.each do |action, short_desc|
99
132
  desc(
100
133
  "#{action} [INSTANCE|REGEXP|all]",
101
- "#{action.capitalize} one or more instances"
134
+ short_desc
102
135
  )
103
- method_option :concurrency, :aliases => "-c",
104
- :type => :numeric, :lazy_default => MAX_CONCURRENCY,
105
- :desc => <<-DESC.gsub(/^\s+/, '').gsub(/\n/, ' ')
136
+ long_desc <<-DESC
137
+ The instance states are in order: destroy, create, converge, setup, verify, destroy.
138
+ Change one or more instances from the current state to the #{action} state. Actions for all
139
+ intermediate states will be executed. See http://kitchen.ci for further explanation.
140
+ DESC
141
+ method_option :concurrency,
142
+ :aliases => "-c",
143
+ :type => :numeric,
144
+ :lazy_default => MAX_CONCURRENCY,
145
+ :desc => <<-DESC.gsub(/^\s+/, "").gsub(/\n/, " ")
106
146
  Run a #{action} against all matching instances concurrently. Only N
107
147
  instances will run at the same time if a number is given.
108
148
  DESC
109
- method_option :parallel, :aliases => "-p", :type => :boolean,
110
- :desc => <<-DESC.gsub(/^\s+/, '').gsub(/\n/, ' ')
149
+ method_option :parallel,
150
+ :aliases => "-p",
151
+ :type => :boolean,
152
+ :desc => <<-DESC.gsub(/^\s+/, "").gsub(/\n/, " ")
111
153
  [Future DEPRECATION, use --concurrency]
112
154
  Run a #{action} against all matching instances concurrently.
113
155
  DESC
114
- method_option :log_level, :aliases => "-l",
156
+ method_option :log_level,
157
+ :aliases => "-l",
115
158
  :desc => "Set the log level (debug, info, warn, error, fatal)"
116
159
  define_method(action) do |*args|
117
160
  update_config!
@@ -119,9 +162,13 @@ module Kitchen
119
162
  end
120
163
  end
121
164
 
122
- desc "test [INSTANCE|REGEXP|all]", "Test one or more instances"
165
+ desc "test [INSTANCE|REGEXP|all]",
166
+ "Test (destroy, create, converge, setup, verify and destroy) one or more instances"
123
167
  long_desc <<-DESC
124
- Test one or more instances
168
+ The instance states are in order: destroy, create, converge, setup, verify, destroy.
169
+ Test changes the state of one or more instances to destroyed, then executes
170
+ the actions for each state up to destroy. At any sign of failure, executing the
171
+ actions stops and the instance is left in the last successful execution state.
125
172
 
126
173
  There are 3 post-verify modes for instance cleanup, triggered with
127
174
  the `--destroy' flag:
@@ -130,22 +177,31 @@ module Kitchen
130
177
  * always: instances will always be destroyed afterwards.\n
131
178
  * never: instances will never be destroyed afterwards.
132
179
  DESC
133
- method_option :concurrency, :aliases => "-c",
134
- :type => :numeric, :lazy_default => MAX_CONCURRENCY,
135
- :desc => <<-DESC.gsub(/^\s+/, '').gsub(/\n/, ' ')
180
+ method_option :concurrency,
181
+ :aliases => "-c",
182
+ :type => :numeric,
183
+ :lazy_default => MAX_CONCURRENCY,
184
+ :desc => <<-DESC.gsub(/^\s+/, "").gsub(/\n/, " ")
136
185
  Run a test against all matching instances concurrently. Only N
137
186
  instances will run at the same time if a number is given.
138
187
  DESC
139
- method_option :parallel, :aliases => "-p", :type => :boolean,
140
- :desc => <<-DESC.gsub(/^\s+/, '').gsub(/\n/, ' ')
188
+ method_option :parallel,
189
+ :aliases => "-p",
190
+ :type => :boolean,
191
+ :desc => <<-DESC.gsub(/^\s+/, "").gsub(/\n/, " ")
141
192
  [Future DEPRECATION, use --concurrency]
142
193
  Run a test against all matching instances concurrently.
143
194
  DESC
144
- method_option :log_level, :aliases => "-l",
195
+ method_option :log_level,
196
+ :aliases => "-l",
145
197
  :desc => "Set the log level (debug, info, warn, error, fatal)"
146
- method_option :destroy, :aliases => "-d", :default => "passing",
198
+ method_option :destroy,
199
+ :aliases => "-d",
200
+ :default => "passing",
147
201
  :desc => "Destroy strategy to use after testing (passing, always, never)."
148
- method_option :auto_init, :type => :boolean, :default => false,
202
+ method_option :auto_init,
203
+ :type => :boolean,
204
+ :default => false,
149
205
  :desc => "Invoke init command if .kitchen.yml is missing"
150
206
  def test(*args)
151
207
  update_config!
@@ -154,18 +210,32 @@ module Kitchen
154
210
  end
155
211
 
156
212
  desc "login INSTANCE|REGEXP", "Log in to one instance"
157
- method_option :log_level, :aliases => "-l",
213
+ method_option :log_level,
214
+ :aliases => "-l",
158
215
  :desc => "Set the log level (debug, info, warn, error, fatal)"
159
216
  def login(*args)
160
217
  update_config!
161
218
  perform("login", "login", args)
162
219
  end
163
220
 
221
+ desc "exec INSTANCE|REGEXP -c REMOTE_COMMAND",
222
+ "Execute command on one or more instance"
223
+ method_option :log_level,
224
+ :aliases => "-l",
225
+ :desc => "Set the log level (debug, info, warn, error, fatal)"
226
+ method_option :command,
227
+ :aliases => "-c",
228
+ :desc => "execute via ssh"
229
+ def exec(*args)
230
+ update_config!
231
+ perform("exec", "exec", args)
232
+ end
233
+
164
234
  desc "version", "Print Kitchen's version information"
165
235
  def version
166
236
  puts "Test Kitchen version #{Kitchen::VERSION}"
167
237
  end
168
- map %w(-v --version) => :version
238
+ map %w[-v --version] => :version
169
239
 
170
240
  desc "sink", "Show the Kitchen sink!", :hide => true
171
241
  def sink
@@ -217,6 +287,7 @@ module Kitchen
217
287
  perform("discover", "driver_discover", args)
218
288
  end
219
289
 
290
+ # @return [String] basename
220
291
  def self.basename
221
292
  super + " driver"
222
293
  end
@@ -238,14 +309,23 @@ module Kitchen
238
309
 
239
310
  private
240
311
 
312
+ # Ensure the any failing commands exit non-zero.
313
+ #
314
+ # @return [true] you die always on failure
315
+ # @api private
241
316
  def self.exit_on_failure?
242
317
  true
243
318
  end
244
319
 
320
+ # @return [Logger] the common logger
321
+ # @api private
245
322
  def logger
246
323
  Kitchen.logger
247
324
  end
248
325
 
326
+ # Update and finalize options for logging, concurrency, and other concerns.
327
+ #
328
+ # @api private
249
329
  def update_config!
250
330
  if options[:log_level]
251
331
  level = options[:log_level].downcase.to_sym
@@ -264,10 +344,13 @@ module Kitchen
264
344
  end
265
345
  end
266
346
 
347
+ # If auto_init option is active, invoke the init generator.
348
+ #
349
+ # @api private
267
350
  def ensure_initialized
268
- yaml = ENV['KITCHEN_YAML'] || '.kitchen.yml'
351
+ yaml = ENV["KITCHEN_YAML"] || ".kitchen.yml"
269
352
 
270
- if options[:auto_init] && ! File.exists?(yaml)
353
+ if options[:auto_init] && !File.exist?(yaml)
271
354
  banner "Invoking init as '#{yaml}' file is missing"
272
355
  invoke "init"
273
356
  end