test-kitchen-rsync 3.0.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +21 -0
  3. data/LICENSE +15 -0
  4. data/Rakefile +53 -0
  5. data/bin/zl-kitchen +11 -0
  6. data/lib/kitchen/base64_stream.rb +48 -0
  7. data/lib/kitchen/chef_utils_wiring.rb +40 -0
  8. data/lib/kitchen/cli.rb +413 -0
  9. data/lib/kitchen/collection.rb +52 -0
  10. data/lib/kitchen/color.rb +63 -0
  11. data/lib/kitchen/command/action.rb +41 -0
  12. data/lib/kitchen/command/console.rb +54 -0
  13. data/lib/kitchen/command/diagnose.rb +84 -0
  14. data/lib/kitchen/command/doctor.rb +39 -0
  15. data/lib/kitchen/command/exec.rb +37 -0
  16. data/lib/kitchen/command/list.rb +148 -0
  17. data/lib/kitchen/command/login.rb +39 -0
  18. data/lib/kitchen/command/package.rb +32 -0
  19. data/lib/kitchen/command/sink.rb +50 -0
  20. data/lib/kitchen/command/test.rb +47 -0
  21. data/lib/kitchen/command.rb +207 -0
  22. data/lib/kitchen/config.rb +344 -0
  23. data/lib/kitchen/configurable.rb +616 -0
  24. data/lib/kitchen/data_munger.rb +1024 -0
  25. data/lib/kitchen/diagnostic.rb +138 -0
  26. data/lib/kitchen/driver/base.rb +133 -0
  27. data/lib/kitchen/driver/dummy.rb +105 -0
  28. data/lib/kitchen/driver/exec.rb +70 -0
  29. data/lib/kitchen/driver/proxy.rb +70 -0
  30. data/lib/kitchen/driver/ssh_base.rb +351 -0
  31. data/lib/kitchen/driver.rb +40 -0
  32. data/lib/kitchen/errors.rb +243 -0
  33. data/lib/kitchen/generator/init.rb +254 -0
  34. data/lib/kitchen/instance.rb +726 -0
  35. data/lib/kitchen/lazy_hash.rb +148 -0
  36. data/lib/kitchen/lifecycle_hook/base.rb +78 -0
  37. data/lib/kitchen/lifecycle_hook/local.rb +53 -0
  38. data/lib/kitchen/lifecycle_hook/remote.rb +39 -0
  39. data/lib/kitchen/lifecycle_hooks.rb +92 -0
  40. data/lib/kitchen/loader/yaml.rb +377 -0
  41. data/lib/kitchen/logger.rb +422 -0
  42. data/lib/kitchen/logging.rb +52 -0
  43. data/lib/kitchen/login_command.rb +49 -0
  44. data/lib/kitchen/metadata_chopper.rb +49 -0
  45. data/lib/kitchen/platform.rb +64 -0
  46. data/lib/kitchen/plugin.rb +76 -0
  47. data/lib/kitchen/plugin_base.rb +60 -0
  48. data/lib/kitchen/provisioner/base.rb +269 -0
  49. data/lib/kitchen/provisioner/chef/berkshelf.rb +116 -0
  50. data/lib/kitchen/provisioner/chef/common_sandbox.rb +350 -0
  51. data/lib/kitchen/provisioner/chef/policyfile.rb +163 -0
  52. data/lib/kitchen/provisioner/chef_apply.rb +121 -0
  53. data/lib/kitchen/provisioner/chef_base.rb +705 -0
  54. data/lib/kitchen/provisioner/chef_infra.rb +167 -0
  55. data/lib/kitchen/provisioner/chef_solo.rb +82 -0
  56. data/lib/kitchen/provisioner/chef_zero.rb +12 -0
  57. data/lib/kitchen/provisioner/dummy.rb +75 -0
  58. data/lib/kitchen/provisioner/shell.rb +157 -0
  59. data/lib/kitchen/provisioner.rb +42 -0
  60. data/lib/kitchen/rake_tasks.rb +80 -0
  61. data/lib/kitchen/shell_out.rb +90 -0
  62. data/lib/kitchen/ssh.rb +289 -0
  63. data/lib/kitchen/state_file.rb +112 -0
  64. data/lib/kitchen/suite.rb +48 -0
  65. data/lib/kitchen/thor_tasks.rb +63 -0
  66. data/lib/kitchen/transport/base.rb +236 -0
  67. data/lib/kitchen/transport/dummy.rb +78 -0
  68. data/lib/kitchen/transport/exec.rb +145 -0
  69. data/lib/kitchen/transport/ssh.rb +579 -0
  70. data/lib/kitchen/transport/winrm.rb +546 -0
  71. data/lib/kitchen/transport.rb +40 -0
  72. data/lib/kitchen/util.rb +229 -0
  73. data/lib/kitchen/verifier/base.rb +243 -0
  74. data/lib/kitchen/verifier/busser.rb +275 -0
  75. data/lib/kitchen/verifier/dummy.rb +75 -0
  76. data/lib/kitchen/verifier/shell.rb +99 -0
  77. data/lib/kitchen/verifier.rb +39 -0
  78. data/lib/kitchen/version.rb +20 -0
  79. data/lib/kitchen/which.rb +26 -0
  80. data/lib/kitchen.rb +152 -0
  81. data/lib/vendor/hash_recursive_merge.rb +79 -0
  82. data/support/busser_install_command.ps1 +14 -0
  83. data/support/busser_install_command.sh +21 -0
  84. data/support/chef-client-fail-if-update-handler.rb +15 -0
  85. data/support/chef_base_init_command.ps1 +18 -0
  86. data/support/chef_base_init_command.sh +1 -0
  87. data/support/chef_base_install_command.ps1 +85 -0
  88. data/support/chef_base_install_command.sh +229 -0
  89. data/support/download_helpers.sh +109 -0
  90. data/support/dummy-validation.pem +27 -0
  91. data/templates/driver/CHANGELOG.md.erb +3 -0
  92. data/templates/driver/Gemfile.erb +3 -0
  93. data/templates/driver/README.md.erb +64 -0
  94. data/templates/driver/Rakefile.erb +21 -0
  95. data/templates/driver/driver.rb.erb +23 -0
  96. data/templates/driver/gemspec.erb +29 -0
  97. data/templates/driver/gitignore.erb +17 -0
  98. data/templates/driver/license_apachev2.erb +15 -0
  99. data/templates/driver/license_lgplv3.erb +16 -0
  100. data/templates/driver/license_mit.erb +22 -0
  101. data/templates/driver/license_reserved.erb +5 -0
  102. data/templates/driver/tailor.erb +4 -0
  103. data/templates/driver/travis.yml.erb +11 -0
  104. data/templates/driver/version.rb.erb +12 -0
  105. data/templates/init/chefignore.erb +2 -0
  106. data/templates/init/kitchen.yml.erb +18 -0
  107. data/test-kitchen.gemspec +52 -0
  108. metadata +528 -0
@@ -0,0 +1,351 @@
1
+ #
2
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
3
+ #
4
+ # Copyright (C) 2012, Fletcher Nichol
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require "thor/util"
19
+
20
+ require_relative "../lazy_hash"
21
+ require_relative "../plugin_base"
22
+ require "benchmark" unless defined?(Benchmark)
23
+
24
+ module Kitchen
25
+ module Driver
26
+ # Legacy base class for a driver that uses SSH to communication with an
27
+ # instance. This class has been updated to use the Instance's Transport to
28
+ # issue commands and transfer files and no longer uses the `Kitchen:SSH`
29
+ # class directly.
30
+ #
31
+ # **NOTE:** Authors of new Drivers are encouraged to inherit from
32
+ # `Kitchen::Driver::Base` instead and existing Driver authors are
33
+ # encouraged to update their Driver class to inherit from
34
+ # `Kitchen::Driver::SSHBase`.
35
+ #
36
+ # A subclass must implement the following methods:
37
+ # * #create(state)
38
+ # * #destroy(state)
39
+ #
40
+ # @author Fletcher Nichol <fnichol@nichol.ca>
41
+ # @deprecated While all possible effort has been made to preserve the
42
+ # original behavior of this class, future improvements to the Driver,
43
+ # Transport, and Verifier subsystems may not be picked up in these
44
+ # Drivers. When legacy Driver::SSHBase support is removed, this class
45
+ # will no longer be available.
46
+ class SSHBase < Kitchen::Plugin::Base
47
+ include ShellOut
48
+ include Configurable
49
+ include Logging
50
+
51
+ default_config :sudo, true
52
+ default_config :port, 22
53
+ # needs to be one less than the configured sshd_config MaxSessions
54
+ default_config :max_ssh_sessions, 9
55
+
56
+ # Creates a new Driver object using the provided configuration data
57
+ # which will be merged with any default configuration.
58
+ #
59
+ # @param config [Hash] provided driver configuration
60
+ def initialize(config = {})
61
+ init_config(config)
62
+ end
63
+
64
+ # (see Base#create)
65
+ def create(state) # rubocop:disable Lint/UnusedMethodArgument
66
+ raise ClientError, "#{self.class}#create must be implemented"
67
+ end
68
+
69
+ # (see Base#converge)
70
+ def converge(state) # rubocop:disable Metrics/AbcSize
71
+ provisioner = instance.provisioner
72
+ provisioner.create_sandbox
73
+ sandbox_dirs = Util.list_directory(provisioner.sandbox_path)
74
+
75
+ instance.transport.connection(backcompat_merged_state(state)) do |conn|
76
+ conn.execute(env_cmd(provisioner.install_command))
77
+ conn.execute(env_cmd(provisioner.init_command))
78
+ info("Transferring files to #{instance.to_str}")
79
+ conn.upload(sandbox_dirs, provisioner[:root_path])
80
+ debug("Transfer complete")
81
+ conn.execute(env_cmd(provisioner.prepare_command))
82
+ conn.execute(env_cmd(provisioner.run_command))
83
+ info("Downloading files from #{instance.to_str}")
84
+ provisioner[:downloads].to_h.each do |remotes, local|
85
+ debug("Downloading #{Array(remotes).join(", ")} to #{local}")
86
+ conn.download(remotes, local)
87
+ end
88
+ debug("Download complete")
89
+ end
90
+ rescue Kitchen::Transport::TransportFailed => ex
91
+ raise ActionFailed, ex.message
92
+ ensure
93
+ instance.provisioner.cleanup_sandbox
94
+ end
95
+
96
+ # (see Base#setup)
97
+ def setup(state)
98
+ verifier = instance.verifier
99
+
100
+ instance.transport.connection(backcompat_merged_state(state)) do |conn|
101
+ conn.execute(env_cmd(verifier.install_command))
102
+ end
103
+ rescue Kitchen::Transport::TransportFailed => ex
104
+ raise ActionFailed, ex.message
105
+ end
106
+
107
+ # (see Base#verify)
108
+ def verify(state) # rubocop:disable Metrics/AbcSize
109
+ verifier = instance.verifier
110
+ verifier.create_sandbox
111
+ sandbox_dirs = Util.list_directory(verifier.sandbox_path)
112
+
113
+ instance.transport.connection(backcompat_merged_state(state)) do |conn|
114
+ conn.execute(env_cmd(verifier.init_command))
115
+ info("Transferring files to #{instance.to_str}")
116
+ conn.upload(sandbox_dirs, verifier[:root_path])
117
+ debug("Transfer complete")
118
+ conn.execute(env_cmd(verifier.prepare_command))
119
+ conn.execute(env_cmd(verifier.run_command))
120
+ end
121
+ rescue Kitchen::Transport::TransportFailed => ex
122
+ raise ActionFailed, ex.message
123
+ ensure
124
+ instance.verifier.cleanup_sandbox
125
+ end
126
+
127
+ # (see Base#destroy)
128
+ def destroy(state) # rubocop:disable Lint/UnusedMethodArgument
129
+ raise ClientError, "#{self.class}#destroy must be implemented"
130
+ end
131
+
132
+ def legacy_state(state)
133
+ backcompat_merged_state(state)
134
+ end
135
+
136
+ # Package an instance.
137
+ #
138
+ # (see Base#package)
139
+ def package(state); end
140
+
141
+ # (see Base#login_command)
142
+ def login_command(state)
143
+ instance.transport.connection(backcompat_merged_state(state))
144
+ .login_command
145
+ end
146
+
147
+ # Executes an arbitrary command on an instance over an SSH connection.
148
+ #
149
+ # @param state [Hash] mutable instance and driver state
150
+ # @param command [String] the command to be executed
151
+ # @raise [ActionFailed] if the command could not be successfully completed
152
+ def remote_command(state, command)
153
+ instance.transport.connection(backcompat_merged_state(state)) do |conn|
154
+ conn.execute(env_cmd(command))
155
+ end
156
+ end
157
+
158
+ # **(Deprecated)** Executes a remote command over SSH.
159
+ #
160
+ # @param ssh_args [Array] ssh arguments
161
+ # @param command [String] remote command to invoke
162
+ # @deprecated This method should no longer be called directly and exists
163
+ # to support very old drivers. This will be removed in the future.
164
+ def ssh(ssh_args, command)
165
+ pseudo_state = { hostname: ssh_args[0], username: ssh_args[1] }
166
+ pseudo_state.merge!(ssh_args[2])
167
+ connection_state = backcompat_merged_state(pseudo_state)
168
+
169
+ instance.transport.connection(connection_state) do |conn|
170
+ conn.execute(env_cmd(command))
171
+ end
172
+ end
173
+
174
+ # Performs whatever tests that may be required to ensure that this driver
175
+ # will be able to function in the current environment. This may involve
176
+ # checking for the presence of certain directories, software installed,
177
+ # etc.
178
+ #
179
+ # @raise [UserError] if the driver will not be able to perform or if a
180
+ # documented dependency is missing from the system
181
+ def verify_dependencies; end
182
+
183
+ # Cache directory that a driver could implement to inform the provisioner
184
+ # that it can leverage it internally
185
+ #
186
+ # @return path [String] a path of the cache directory
187
+ def cache_directory; end
188
+
189
+ private
190
+
191
+ def backcompat_merged_state(state)
192
+ driver_ssh_keys = %w{
193
+ forward_agent hostname password port ssh_key username
194
+ }.map(&:to_sym)
195
+ config.select { |key, _| driver_ssh_keys.include?(key) }.rmerge(state)
196
+ end
197
+
198
+ # Builds arguments for constructing a `Kitchen::SSH` instance.
199
+ #
200
+ # @param state [Hash] state hash
201
+ # @return [Array] SSH constructor arguments
202
+ # @api private
203
+ def build_ssh_args(state)
204
+ combined = config.to_hash.merge(state)
205
+
206
+ opts = {}
207
+ opts[:user_known_hosts_file] = "/dev/null"
208
+ opts[:verify_host_key] = false
209
+ opts[:keys_only] = true if combined[:ssh_key]
210
+ opts[:password] = combined[:password] if combined[:password]
211
+ opts[:forward_agent] = combined[:forward_agent] if combined.key? :forward_agent
212
+ opts[:port] = combined[:port] if combined[:port]
213
+ opts[:keys] = Array(combined[:ssh_key]) if combined[:ssh_key]
214
+ opts[:logger] = logger
215
+
216
+ [combined[:hostname], combined[:username], opts]
217
+ end
218
+
219
+ # Adds http, https and ftp proxy environment variables to a command, if
220
+ # set in configuration data or on local workstation.
221
+ #
222
+ # @param cmd [String] command string
223
+ # @return [String] command string
224
+ # @api private
225
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize
226
+ def env_cmd(cmd)
227
+ return if cmd.nil?
228
+
229
+ env = "env"
230
+ http_proxy = config[:http_proxy] || ENV["http_proxy"] ||
231
+ ENV["HTTP_PROXY"]
232
+ https_proxy = config[:https_proxy] || ENV["https_proxy"] ||
233
+ ENV["HTTPS_PROXY"]
234
+ ftp_proxy = config[:ftp_proxy] || ENV["ftp_proxy"] ||
235
+ ENV["FTP_PROXY"]
236
+ no_proxy = if (!config[:http_proxy] && http_proxy) ||
237
+ (!config[:https_proxy] && https_proxy) ||
238
+ (!config[:ftp_proxy] && ftp_proxy)
239
+ ENV["no_proxy"] || ENV["NO_PROXY"]
240
+ end
241
+ env << " http_proxy=#{http_proxy}" if http_proxy
242
+ env << " https_proxy=#{https_proxy}" if https_proxy
243
+ env << " ftp_proxy=#{ftp_proxy}" if ftp_proxy
244
+ env << " no_proxy=#{no_proxy}" if no_proxy
245
+
246
+ env == "env" ? cmd : "#{env} #{cmd}"
247
+ end
248
+
249
+ # Executes a remote command over SSH.
250
+ #
251
+ # @param command [String] remove command to run
252
+ # @param connection [Kitchen::SSH] an SSH connection
253
+ # @raise [ActionFailed] if an exception occurs
254
+ # @api private
255
+ def run_remote(command, connection)
256
+ return if command.nil?
257
+
258
+ connection.exec(env_cmd(command))
259
+ rescue SSHFailed, Net::SSH::Exception => ex
260
+ raise ActionFailed, ex.message
261
+ end
262
+
263
+ # Transfers one or more local paths over SSH.
264
+ #
265
+ # @param locals [Array<String>] array of local paths
266
+ # @param remote [String] remote destination path
267
+ # @param connection [Kitchen::SSH] an SSH connection
268
+ # @raise [ActionFailed] if an exception occurs
269
+ # @api private
270
+ def transfer_path(locals, remote, connection)
271
+ return if locals.nil? || Array(locals).empty?
272
+
273
+ info("Transferring files to #{instance.to_str}")
274
+ debug("TIMING: scp asynch upload (Kitchen::Driver::SSHBase)")
275
+ elapsed = Benchmark.measure do
276
+ transfer_path_async(locals, remote, connection)
277
+ end
278
+ delta = Util.duration(elapsed.real)
279
+ debug("TIMING: scp async upload (Kitchen::Driver::SSHBase) took #{delta}")
280
+ debug("Transfer complete")
281
+ rescue SSHFailed, Net::SSH::Exception => ex
282
+ raise ActionFailed, ex.message
283
+ end
284
+
285
+ def transfer_path_async(locals, remote, connection)
286
+ waits = []
287
+ locals.map do |local|
288
+ waits.push connection.upload_path(local, remote)
289
+ waits.shift.wait while waits.length >= config[:max_ssh_sessions]
290
+ end
291
+ waits.each(&:wait)
292
+ end
293
+
294
+ # Blocks until a TCP socket is available where a remote SSH server
295
+ # should be listening.
296
+ #
297
+ # @param hostname [String] remote SSH server host
298
+ # @param username [String] SSH username (default: `nil`)
299
+ # @param options [Hash] configuration hash (default: `{}`)
300
+ # @api private
301
+ def wait_for_sshd(hostname, username = nil, options = {})
302
+ pseudo_state = { hostname: hostname }
303
+ pseudo_state[:username] = username if username
304
+ pseudo_state.merge!(options)
305
+
306
+ instance.transport.connection(backcompat_merged_state(pseudo_state))
307
+ .wait_until_ready
308
+ end
309
+
310
+ # Intercepts any bare #puts calls in subclasses and issues an INFO log
311
+ # event instead.
312
+ #
313
+ # @param msg [String] message string
314
+ def puts(msg)
315
+ info(msg)
316
+ end
317
+
318
+ # Intercepts any bare #print calls in subclasses and issues an INFO log
319
+ # event instead.
320
+ #
321
+ # @param msg [String] message string
322
+ def print(msg)
323
+ info(msg)
324
+ end
325
+
326
+ # Delegates to Kitchen::ShellOut.run_command, overriding some default
327
+ # options:
328
+ #
329
+ # * `:use_sudo` defaults to the value of `config[:use_sudo]` in the
330
+ # Driver object
331
+ # * `:log_subject` defaults to a String representation of the Driver's
332
+ # class name
333
+ #
334
+ # @see ShellOut#run_command
335
+ def run_command(cmd, options = {})
336
+ base_options = {
337
+ use_sudo: config[:use_sudo],
338
+ log_subject: Thor::Util.snake_case(self.class.to_s),
339
+ }.merge(options)
340
+ super(cmd, base_options)
341
+ end
342
+
343
+ # Returns the Busser object associated with the driver.
344
+ #
345
+ # @return [Busser] a busser
346
+ def busser
347
+ instance.verifier
348
+ end
349
+ end
350
+ end
351
+ end
@@ -0,0 +1,40 @@
1
+ #
2
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
3
+ #
4
+ # Copyright (C) 2012, Fletcher Nichol
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require_relative "plugin"
19
+
20
+ module Kitchen
21
+ # A driver is responsible for carrying out the lifecycle activities of an
22
+ # instance, such as creating and destroying an instance.
23
+ #
24
+ # @author Fletcher Nichol <fnichol@nichol.ca>
25
+ module Driver
26
+ # Default driver plugin to use
27
+ DEFAULT_PLUGIN = "dummy".freeze
28
+
29
+ # Returns an instance of a driver given a plugin type string.
30
+ #
31
+ # @param plugin [String] a driver plugin type, which will be constantized
32
+ # @param config [Hash] a configuration hash to initialize the driver
33
+ # @return [Driver::Base] a driver instance
34
+ # @raise [ClientError] if a driver instance could not be created
35
+ # @raise [UserError] if the driver's dependencies could not be met
36
+ def self.for_plugin(plugin, config)
37
+ Kitchen::Plugin.load(self, plugin, config)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,243 @@
1
+ #
2
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
3
+ #
4
+ # Copyright (C) 2013, Fletcher Nichol
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require "English"
19
+
20
+ module Kitchen
21
+ # All Kitchen errors and exceptions.
22
+ #
23
+ # @author Fletcher Nichol <fnichol@nichol.ca>
24
+ module Error
25
+ # Creates an array of strings, representing a formatted exception,
26
+ # containing backtrace and nested exception info as necessary, that can
27
+ # be viewed by a human.
28
+ #
29
+ # For example:
30
+ #
31
+ # ------Exception-------
32
+ # Class: Kitchen::StandardError
33
+ # Message: Failure starting the party
34
+ # ---Nested Exception---
35
+ # Class: IOError
36
+ # Message: not enough directories for a party
37
+ # ------Backtrace-------
38
+ # nil
39
+ # ----------------------
40
+ #
41
+ # @param exception [::StandardError] an exception
42
+ # @return [Array<String>] a formatted message
43
+ def self.formatted_trace(exception, title = "Exception")
44
+ arr = formatted_exception(exception, title).dup
45
+ arr += formatted_backtrace(exception)
46
+
47
+ if exception.respond_to?(:original) && exception.original
48
+ arr += if exception.original.is_a? Array
49
+ exception.original.map do |composite_exception|
50
+ formatted_trace(composite_exception, "Composite Exception").flatten
51
+ end
52
+ else
53
+ [
54
+ formatted_exception(exception.original, "Nested Exception"),
55
+ formatted_backtrace(exception),
56
+ ].flatten
57
+ end
58
+ end
59
+ arr.flatten
60
+ end
61
+
62
+ def self.formatted_backtrace(exception)
63
+ if exception.backtrace.nil?
64
+ []
65
+ else
66
+ [
67
+ "Backtrace".center(22, "-"),
68
+ exception.backtrace,
69
+ "End Backtrace".center(22, "-"),
70
+ ]
71
+ end
72
+ end
73
+
74
+ # Creates an array of strings, representing a formatted exception that
75
+ # can be viewed by a human. Thanks to MiniTest for the inspiration
76
+ # upon which this output has been designed.
77
+ #
78
+ # For example:
79
+ #
80
+ # ------Exception-------
81
+ # Class: Kitchen::StandardError
82
+ # Message: I have failed you
83
+ # ----------------------
84
+ #
85
+ # @param exception [::StandardError] an exception
86
+ # @param title [String] a custom title for the message
87
+ # (default: `"Exception"`)
88
+ # @return [Array<String>] a formatted message
89
+ def self.formatted_exception(exception, title = "Exception")
90
+ [
91
+ title.center(22, "-"),
92
+ "Class: #{exception.class}",
93
+ "Message: #{exception.message}",
94
+ "".center(22, "-"),
95
+ ]
96
+ end
97
+ end
98
+
99
+ # Base exception class from which all Kitchen exceptions derive. This class
100
+ # nests an exception when this class is re-raised from a rescue block.
101
+ class StandardError < ::StandardError
102
+ include Error
103
+
104
+ # @return [::StandardError] the original (wrapped) exception
105
+ attr_reader :original
106
+
107
+ # Creates a new StandardError exception which optionally wraps an original
108
+ # exception if given or detected by checking the `$!` global variable.
109
+ #
110
+ # @param msg [String] exception message
111
+ # @param original [::StandardError] an original exception which will be
112
+ # wrapped (default: `$ERROR_INFO`)
113
+ def initialize(msg, original = $ERROR_INFO)
114
+ super(msg)
115
+ @original = original
116
+ end
117
+ end
118
+
119
+ # Base exception class for all exceptions that are caused by user input
120
+ # errors.
121
+ class UserError < StandardError; end
122
+
123
+ # Base exception class for all exceptions that are caused by incorrect use
124
+ # of an API.
125
+ class ClientError < StandardError; end
126
+
127
+ # Base exception class for exceptions that are caused by external library
128
+ # failures which may be temporary.
129
+ class TransientFailure < StandardError; end
130
+
131
+ # Exception class for any exceptions raised when performing an instance
132
+ # action.
133
+ class ActionFailed < TransientFailure; end
134
+
135
+ # Exception class capturing what caused an instance to die.
136
+ class InstanceFailure < TransientFailure; end
137
+
138
+ # Yields to a code block in order to consistently emit a useful crash/error
139
+ # message and exit appropriately. There are two primary failure conditions:
140
+ # an expected instance failure, and any other unexpected failures.
141
+ #
142
+ # **Note** This method may call `Kernel.exit` so may not return if the
143
+ # yielded code block raises an exception.
144
+ #
145
+ # ## Instance Failure
146
+ #
147
+ # This is an expected failure scenario which could happen if an instance
148
+ # couldn't be created, a Chef run didn't successfully converge, a
149
+ # post-convergence test suite failed, etc. In other words, you can count on
150
+ # encountering these failures all the time--this is Kitchen's worldview:
151
+ # crash early and often. In this case a cleanly formatted exception is
152
+ # written to `STDERR` and the exception message is written to
153
+ # the common Kitchen file logger.
154
+ #
155
+ # ## Unexpected Failure
156
+ #
157
+ # All other forms of `Kitchen::Error` exceptions are considered unexpected
158
+ # or unplanned exceptions, typically from user configuration errors, driver
159
+ # or provisioner coding issues or bugs, or internal code issues. Given
160
+ # a stable release of Kitchen and a solid set of drivers and provisioners,
161
+ # the most likely cause of this is user configuration error originating in
162
+ # the `.kitchen.yml` setup. For this reason, the exception is written to
163
+ # `STDERR`, a full formatted exception trace is written to the common
164
+ # Kitchen file logger, and a message is displayed on `STDERR` to the user
165
+ # informing them to check the log files and check their configuration with
166
+ # the `kitchen diagnose` subcommand.
167
+ #
168
+ # @raise [SystemExit] if an exception is raised in the yielded block
169
+ def self.with_friendly_errors
170
+ yield
171
+ rescue Kitchen::InstanceFailure => e
172
+ Kitchen.mutex.synchronize do
173
+ handle_instance_failure(e)
174
+ end
175
+ exit 10
176
+ rescue Kitchen::Error => e
177
+ Kitchen.mutex.synchronize do
178
+ handle_error(e)
179
+ end
180
+ exit 20
181
+ end
182
+
183
+ # Writes an array of lines to the common Kitchen logger's file device at the
184
+ # given severity level. If the Kitchen logger is set to debug severity, then
185
+ # the array of lines will also be written to the console output.
186
+ #
187
+ # @param level [Symbol,String] the desired log level
188
+ # @param lines [Array<String>] an array of strings to log
189
+ # @api private
190
+ def self.file_log(level, lines)
191
+ Array(lines).each do |line|
192
+ if Kitchen.logger.debug?
193
+ Kitchen.logger.debug(line)
194
+ else
195
+ Kitchen.logger.logdev && Kitchen.logger.logdev.public_send(level, line)
196
+ end
197
+ end
198
+ end
199
+
200
+ # Writes an array of lines to the `STDERR` device.
201
+ #
202
+ # @param lines [Array<String>] an array of strings to log
203
+ # @api private
204
+ def self.stderr_log(lines)
205
+ Array(lines).map { |line| ">>>>>> #{line}" }.each do |line|
206
+ line = Color.colorize(line, :red) if Kitchen.tty?
207
+ $stderr.puts(line)
208
+ end
209
+ end
210
+
211
+ # Writes an array of lines to the common Kitchen debugger with debug
212
+ # severity.
213
+ #
214
+ # @param lines [Array<String>] an array of strings to log
215
+ # @api private
216
+ def self.debug_log(lines)
217
+ Array(lines).each { |line| Kitchen.logger.debug(line) }
218
+ end
219
+
220
+ # Handles an instance failure exception.
221
+ #
222
+ # @param e [StandardError] an exception to handle
223
+ # @see Kitchen.with_friendly_errors
224
+ # @api private
225
+ def self.handle_instance_failure(e)
226
+ stderr_log(e.message.split(/\s{2,}/))
227
+ stderr_log(Error.formatted_exception(e.original))
228
+ file_log(:error, e.message.split(/\s{2,}/).first)
229
+ debug_log(Error.formatted_trace(e))
230
+ end
231
+
232
+ # Handles an unexpected failure exception.
233
+ #
234
+ # @param e [StandardError] an exception to handle
235
+ # @see Kitchen.with_friendly_errors
236
+ # @api private
237
+ def self.handle_error(e)
238
+ stderr_log(Error.formatted_exception(e))
239
+ stderr_log("Please see .kitchen/logs/kitchen.log for more details")
240
+ stderr_log("Also try running `kitchen diagnose --all` for configuration\n")
241
+ file_log(:error, Error.formatted_trace(e))
242
+ end
243
+ end