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
@@ -16,7 +16,8 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
- require 'thor/group'
19
+ require "rubygems/gem_runner"
20
+ require "thor/group"
20
21
 
21
22
  module Kitchen
22
23
 
@@ -30,143 +31,202 @@ module Kitchen
30
31
 
31
32
  include Thor::Actions
32
33
 
33
- class_option :driver, :type => :array, :aliases => "-D",
34
+ class_option :driver,
35
+ :type => :array,
36
+ :aliases => "-D",
34
37
  :default => "kitchen-vagrant",
35
- :desc => <<-D.gsub(/^\s+/, '').gsub(/\n/, ' ')
38
+ :desc => <<-D.gsub(/^\s+/, "").gsub(/\n/, " ")
36
39
  One or more Kitchen Driver gems to be installed or added to a
37
40
  Gemfile
38
41
  D
39
42
 
40
- class_option :provisioner, :type => :string, :aliases => "-P",
43
+ class_option :provisioner,
44
+ :type => :string,
45
+ :aliases => "-P",
41
46
  :default => "chef_solo",
42
- :desc => <<-D.gsub(/^\s+/, '').gsub(/\n/, ' ')
47
+ :desc => <<-D.gsub(/^\s+/, "").gsub(/\n/, " ")
43
48
  The default Kitchen Provisioner to use
44
49
  D
45
50
 
46
- class_option :create_gemfile, :type => :boolean, :default => false,
47
- :desc => <<-D.gsub(/^\s+/, '').gsub(/\n/, ' ')
51
+ class_option :create_gemfile,
52
+ :type => :boolean,
53
+ :default => false,
54
+ :desc => <<-D.gsub(/^\s+/, "").gsub(/\n/, " ")
48
55
  Whether or not to create a Gemfile if one does not exist.
49
56
  Default: false
50
57
  D
51
58
 
59
+ # Invoke the command.
52
60
  def init
53
61
  self.class.source_root(Kitchen.source_root.join("templates", "init"))
54
62
 
55
63
  create_kitchen_yaml
56
- prepare_rakefile if init_rakefile?
57
- prepare_thorfile if init_thorfile?
58
- empty_directory "test/integration/default" if init_test_dir?
59
- if init_git?
60
- append_to_gitignore(".kitchen/")
61
- append_to_gitignore(".kitchen.local.yml")
62
- end
63
- prepare_gemfile if init_gemfile?
64
+ prepare_rakefile
65
+ prepare_thorfile
66
+ create_test_dir
67
+ prepare_gitignore
68
+ prepare_gemfile
64
69
  add_drivers
65
-
66
- if @display_bundle_msg
67
- say "You must run `bundle install' to fetch any new gems.", :red
68
- end
70
+ display_bundle_message
69
71
  end
70
72
 
71
73
  private
72
74
 
75
+ # Creates the `.kitchen.yml` file.
76
+ #
77
+ # @api private
73
78
  def create_kitchen_yaml
74
- cookbook_name = if File.exists?(File.expand_path('metadata.rb'))
75
- MetadataChopper.extract('metadata.rb').first
79
+ cookbook_name = if File.exist?(File.expand_path("metadata.rb"))
80
+ MetadataChopper.extract("metadata.rb").first
76
81
  else
77
82
  nil
78
83
  end
79
84
  run_list = cookbook_name ? "recipe[#{cookbook_name}::default]" : nil
80
- driver_plugin = Array(options[:driver]).first || 'dummy'
85
+ driver_plugin = Array(options[:driver]).first || "dummy"
81
86
 
82
- template("kitchen.yml.erb", ".kitchen.yml", {
83
- :driver_plugin => driver_plugin.sub(/^kitchen-/, ''),
87
+ template("kitchen.yml.erb", ".kitchen.yml",
88
+ :driver_plugin => driver_plugin.sub(/^kitchen-/, ""),
84
89
  :provisioner => options[:provisioner],
85
90
  :run_list => Array(run_list)
86
- })
91
+ )
87
92
  end
88
93
 
94
+ # @return [true,false] whether or not a Gemfile needs to be initialized
95
+ # @api private
89
96
  def init_gemfile?
90
- File.exists?(File.join(destination_root, "Gemfile")) ||
97
+ File.exist?(File.join(destination_root, "Gemfile")) ||
91
98
  options[:create_gemfile]
92
99
  end
93
100
 
101
+ # @return [true,false] whether or not a Rakefile needs to be initialized
102
+ # @api private
94
103
  def init_rakefile?
95
- File.exists?(File.join(destination_root, "Rakefile")) &&
104
+ File.exist?(File.join(destination_root, "Rakefile")) &&
96
105
  not_in_file?("Rakefile", %r{require 'kitchen/rake_tasks'})
97
106
  end
98
107
 
108
+ # @return [true,false] whether or not a Thorfile needs to be initialized
109
+ # @api private
99
110
  def init_thorfile?
100
- File.exists?(File.join(destination_root, "Thorfile")) &&
111
+ File.exist?(File.join(destination_root, "Thorfile")) &&
101
112
  not_in_file?("Thorfile", %r{require 'kitchen/thor_tasks'})
102
113
  end
103
114
 
115
+ # @return [true,false] whether or not a test directory needs to be
116
+ # initialized
117
+ # @api private
104
118
  def init_test_dir?
105
119
  Dir.glob("test/integration/*").select { |d| File.directory?(d) }.empty?
106
120
  end
107
121
 
122
+ # @return [true,false] whether or not a `.gitignore` file needs to be
123
+ # initialized
124
+ # @api private
108
125
  def init_git?
109
- File.directory?(File.join(destination_root, '.git'))
126
+ File.directory?(File.join(destination_root, ".git"))
110
127
  end
111
128
 
129
+ # Prepares a Rakefile.
130
+ #
131
+ # @api private
112
132
  def prepare_rakefile
113
- rakedoc = <<-RAKE.gsub(/^ {10}/, '')
133
+ return unless init_rakefile?
134
+
135
+ rakedoc = <<-RAKE.gsub(/^ {10}/, "")
114
136
 
115
137
  begin
116
- require 'kitchen/rake_tasks'
138
+ require "kitchen/rake_tasks"
117
139
  Kitchen::RakeTasks.new
118
140
  rescue LoadError
119
- puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV['CI']
141
+ puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV["CI"]
120
142
  end
121
143
  RAKE
122
144
  append_to_file(File.join(destination_root, "Rakefile"), rakedoc)
123
145
  end
124
146
 
147
+ # Prepares a Thorfile.
148
+ #
149
+ # @api private
125
150
  def prepare_thorfile
126
- thordoc = <<-THOR.gsub(/^ {10}/, '')
151
+ return unless init_thorfile?
152
+
153
+ thordoc = <<-THOR.gsub(/^ {10}/, "")
127
154
 
128
155
  begin
129
- require 'kitchen/thor_tasks'
156
+ require "kitchen/thor_tasks"
130
157
  Kitchen::ThorTasks.new
131
158
  rescue LoadError
132
- puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV['CI']
159
+ puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV["CI"]
133
160
  end
134
161
  THOR
135
162
  append_to_file(File.join(destination_root, "Thorfile"), thordoc)
136
163
  end
137
164
 
165
+ # Create the default test directory
166
+ #
167
+ # @api private
168
+ def create_test_dir
169
+ empty_directory "test/integration/default" if init_test_dir?
170
+ end
171
+
172
+ # Prepares the .gitignore file
173
+ #
174
+ # @api private
175
+ def prepare_gitignore
176
+ return unless init_git?
177
+
178
+ append_to_gitignore(".kitchen/")
179
+ append_to_gitignore(".kitchen.local.yml")
180
+ end
181
+
182
+ # Appends a line to the .gitignore file.
183
+ #
184
+ # @api private
138
185
  def append_to_gitignore(line)
139
- create_file(".gitignore") unless File.exists?(File.join(destination_root, ".gitignore"))
186
+ create_file(".gitignore") unless File.exist?(File.join(destination_root, ".gitignore"))
140
187
 
141
188
  if IO.readlines(File.join(destination_root, ".gitignore")).grep(%r{^#{line}}).empty?
142
189
  append_to_file(".gitignore", "#{line}\n")
143
190
  end
144
191
  end
145
192
 
193
+ # Prepares a Gemfile.
194
+ #
195
+ # @api private
146
196
  def prepare_gemfile
197
+ return unless init_gemfile?
198
+
147
199
  create_gemfile_if_missing
148
200
  add_gem_to_gemfile
149
201
  end
150
202
 
203
+ # Creates a Gemfile if missing
204
+ #
205
+ # @api private
151
206
  def create_gemfile_if_missing
152
- unless File.exists?(File.join(destination_root, "Gemfile"))
153
- create_file("Gemfile", %{source 'https://rubygems.org'\n\n})
207
+ unless File.exist?(File.join(destination_root, "Gemfile"))
208
+ create_file("Gemfile", %{source "https://rubygems.org"\n\n})
154
209
  end
155
210
  end
156
211
 
212
+ # Appends entries to a Gemfile.
213
+ #
214
+ # @api private
157
215
  def add_gem_to_gemfile
158
216
  if not_in_file?("Gemfile", %r{gem ('|")test-kitchen('|")})
159
- append_to_file("Gemfile", %{gem 'test-kitchen'\n})
217
+ append_to_file("Gemfile", %{gem "test-kitchen"\n})
160
218
  @display_bundle_msg = true
161
219
  end
162
220
  end
163
221
 
222
+ # Appends driver gems to a Gemfile or installs them.
223
+ #
224
+ # @api private
164
225
  def add_drivers
165
226
  return if options[:driver].nil? || options[:driver].empty?
166
- display_warning = false
167
227
 
168
228
  Array(options[:driver]).each do |driver_gem|
169
- if File.exists?(File.join(destination_root, "Gemfile")) || options[:create_gemfile]
229
+ if File.exist?(File.join(destination_root, "Gemfile")) || options[:create_gemfile]
170
230
  add_driver_to_gemfile(driver_gem)
171
231
  else
172
232
  install_gem(driver_gem)
@@ -174,27 +234,52 @@ module Kitchen
174
234
  end
175
235
  end
176
236
 
237
+ # Appends a driver gem to a Gemfile.
238
+ #
239
+ # @api private
177
240
  def add_driver_to_gemfile(driver_gem)
178
241
  if not_in_file?("Gemfile", %r{gem ('|")#{driver_gem}('|")})
179
- append_to_file("Gemfile", %{gem '#{driver_gem}'\n})
242
+ append_to_file("Gemfile", %{gem "#{driver_gem}"\n})
180
243
  @display_bundle_msg = true
181
244
  end
182
245
  end
183
246
 
247
+ # Installs a driver gem.
248
+ #
249
+ # @api private
184
250
  def install_gem(driver_gem)
185
- unbundlerize do
186
- run "gem install #{driver_gem}"
251
+ unbundlerize { Gem::GemRunner.new.run(["install", driver_gem]) }
252
+ rescue Gem::SystemExitException => e
253
+ raise unless e.exit_code == 0
254
+ end
255
+
256
+ # Displays a bundle warning message to the user.
257
+ #
258
+ # @api private
259
+ def display_bundle_message
260
+ if @display_bundle_msg
261
+ say "You must run `bundle install' to fetch any new gems.", :red
187
262
  end
188
263
  end
189
264
 
265
+ # Determines whether or not a pattern is found in a file.
266
+ #
267
+ # @param filename [String] filename to read
268
+ # @param regexp [Regexp] a regular expression
269
+ # @return [true,false] whether or not a pattern is found in a file
270
+ # @api private
190
271
  def not_in_file?(filename, regexp)
191
272
  IO.readlines(File.join(destination_root, filename)).grep(regexp).empty?
192
273
  end
193
274
 
275
+ # Save off any Bundler/Ruby-related environment variables so that the
276
+ # yielded block can run "bundler-free" (and restore at the end).
277
+ #
278
+ # @api private
194
279
  def unbundlerize
195
280
  keys = ENV.keys.select { |key| key =~ /^BUNDLER?_/ } + %w[RUBYOPT]
196
281
 
197
- keys.each { |key| ENV["__#{key}"] = ENV[key] ; ENV.delete(key) }
282
+ keys.each { |key| ENV["__#{key}"] = ENV[key]; ENV.delete(key) }
198
283
  yield
199
284
  keys.each { |key| ENV[key] = ENV.delete("__#{key}") }
200
285
  end
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
4
  #
5
- # Copyright (C) 2012, Fletcher Nichol
5
+ # Copyright (C) 2012, 2013, 2014, Fletcher Nichol
6
6
  #
7
7
  # Licensed under the Apache License, Version 2.0 (the "License");
8
8
  # you may not use this file except in compliance with the License.
@@ -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 'benchmark'
20
- require 'fileutils'
19
+ require "benchmark"
20
+ require "fileutils"
21
21
 
22
22
  module Kitchen
23
23
 
@@ -31,10 +31,18 @@ module Kitchen
31
31
  include Logging
32
32
 
33
33
  class << self
34
+
35
+ # @return [Hash] a hash of mutxes, arranged by Driver class names
36
+ # @api private
34
37
  attr_accessor :mutexes
35
38
 
39
+ # Generates a name for an instance given a suite and platform.
40
+ #
41
+ # @param suite [Suite,#name] a Suite
42
+ # @param platform [Platform,#name] a Platform
43
+ # @return [String] a normalized, consistent name for an instance
36
44
  def name_for(suite, platform)
37
- "#{suite.name}-#{platform.name}".gsub(/_/, '-').gsub(/\./, '')
45
+ "#{suite.name}-#{platform.name}".gsub(%r{[_,/]}, "-").gsub(/\./, "")
38
46
  end
39
47
  end
40
48
 
@@ -66,11 +74,11 @@ module Kitchen
66
74
  # Creates a new instance, given a suite and a platform.
67
75
  #
68
76
  # @param [Hash] options configuration for a new suite
69
- # @option options [Suite] :suite the suite (**Required)
70
- # @option options [Platform] :platform the platform (**Required)
71
- # @option options [Driver::Base] :driver the driver (**Required)
77
+ # @option options [Suite] :suite the suite (**Required**)
78
+ # @option options [Platform] :platform the platform (**Required**)
79
+ # @option options [Driver::Base] :driver the driver (**Required**)
72
80
  # @option options [Provisioner::Base] :provisioner the provisioner
73
- # (**Required)
81
+ # (**Required**)
74
82
  # @option options [Busser] :busser the busser logger (**Required**)
75
83
  # @option options [Logger] :logger the instance logger
76
84
  # (default: Kitchen.logger)
@@ -93,6 +101,9 @@ module Kitchen
93
101
  setup_provisioner
94
102
  end
95
103
 
104
+ # Returns a displayable representation of the instance.
105
+ #
106
+ # @return [String] an instance display string
96
107
  def to_str
97
108
  "<#{name}>"
98
109
  end
@@ -185,12 +196,24 @@ module Kitchen
185
196
  # @see Driver::LoginCommand
186
197
  # @see Driver::Base#login_command
187
198
  def login
188
- login_command = driver.login_command(state_file.read)
189
- command, *args = login_command.cmd_array
199
+ state = state_file.read
200
+ if state[:last_action].nil?
201
+ raise UserError, "Instance #{to_str} has not yet been created"
202
+ end
203
+
204
+ login_command = driver.login_command(state)
205
+ cmd, *args = login_command.cmd_array
190
206
  options = login_command.options
191
207
 
192
- debug("Login command: #{command} #{args.join(' ')} (Options: #{options})")
193
- Kernel.exec(command, *args, options)
208
+ debug(%{Login command: #{cmd} #{args.join(" ")} (Options: #{options})})
209
+ Kernel.exec(cmd, *args, options)
210
+ end
211
+
212
+ # Executes an arbitrary command on this instance.
213
+ #
214
+ # @param command [String] a command string to execute
215
+ def remote_exec(command)
216
+ driver.remote_command(state_file.read, command)
194
217
  end
195
218
 
196
219
  # Returns a Hash of configuration and other useful diagnostic information.
@@ -205,29 +228,43 @@ module Kitchen
205
228
  result
206
229
  end
207
230
 
231
+ # Returns the last successfully completed action state of the instance.
232
+ #
233
+ # @return [String] a named action which was last successfully completed
208
234
  def last_action
209
235
  state_file.read[:last_action]
210
236
  end
211
237
 
212
238
  private
213
239
 
240
+ # @return [StateFile] a state file object that can be read from or written
241
+ # to
242
+ # @api private
214
243
  attr_reader :state_file
215
244
 
245
+ # Validate the initial internal state of this object and raising an
246
+ # exception if any preconditions are not met.
247
+ #
248
+ # @param options[Hash] options hash passed into the constructor
249
+ # @raise [ClientError] if any validations fail
250
+ # @api private
216
251
  def validate_options(options)
217
- [:suite, :platform, :driver, :provisioner, :busser, :state_file].each do |k|
218
- if !options.has_key?(k)
219
- raise ClientError, "Instance#new requires option :#{k}"
220
- end
252
+ [
253
+ :suite, :platform, :driver, :provisioner, :busser, :state_file
254
+ ].each do |k|
255
+ next if options.key?(k)
256
+
257
+ raise ClientError, "Instance#new requires option :#{k}"
221
258
  end
222
259
  end
223
260
 
261
+ # Perform any final configuration or preparation needed for the driver
262
+ # object carry out its duties.
263
+ #
264
+ # @api private
224
265
  def setup_driver
225
- @driver.instance = self
226
- @driver.validate_config!
227
- setup_driver_mutex
228
- end
266
+ @driver.finalize_config!(self)
229
267
 
230
- def setup_driver_mutex
231
268
  if driver.class.serial_actions
232
269
  Kitchen.mutex.synchronize do
233
270
  self.class.mutexes ||= Hash.new
@@ -236,10 +273,19 @@ module Kitchen
236
273
  end
237
274
  end
238
275
 
276
+ # Perform any final configuration or preparation needed for the provisioner
277
+ # object carry out its duties.
278
+ #
279
+ # @api private
239
280
  def setup_provisioner
240
- @provisioner.instance = self
281
+ @provisioner.finalize_config!(self)
241
282
  end
242
283
 
284
+ # Perform all actions in order from last state to desired state.
285
+ #
286
+ # @param desired [Symbol] a symbol representing the desired action state
287
+ # @return [self] this instance, used to chain actions
288
+ # @api private
243
289
  def transition_to(desired)
244
290
  result = nil
245
291
  FSM.actions(last_action, desired).each do |transition|
@@ -248,35 +294,87 @@ module Kitchen
248
294
  result
249
295
  end
250
296
 
297
+ # Perform the create action.
298
+ #
299
+ # @see Driver::Base#create
300
+ # @return [self] this instance, used to chain actions
301
+ # @api private
251
302
  def create_action
252
303
  perform_action(:create, "Creating")
253
304
  end
254
305
 
306
+ # Perform the converge action.
307
+ #
308
+ # @see Driver::Base#converge
309
+ # @return [self] this instance, used to chain actions
310
+ # @api private
255
311
  def converge_action
256
312
  perform_action(:converge, "Converging")
257
313
  end
258
314
 
315
+ # Perform the setup action.
316
+ #
317
+ # @see Driver::Base#setup
318
+ # @return [self] this instance, used to chain actions
319
+ # @api private
259
320
  def setup_action
260
321
  perform_action(:setup, "Setting up")
261
322
  end
262
323
 
324
+ # Perform the verify action.
325
+ #
326
+ # @see Driver::Base#verify
327
+ # @return [self] this instance, used to chain actions
328
+ # @api private
263
329
  def verify_action
264
330
  perform_action(:verify, "Verifying")
265
331
  end
266
332
 
333
+ # Perform the destroy action.
334
+ #
335
+ # @see Driver::Base#destroy
336
+ # @return [self] this instance, used to chain actions
337
+ # @api private
267
338
  def destroy_action
268
339
  perform_action(:destroy, "Destroying") { state_file.destroy }
269
340
  end
270
341
 
342
+ # Perform an arbitrary action and provide useful logging.
343
+ #
344
+ # @param verb [Symbol] the action to be performed
345
+ # @param output_verb [String] a verb representing the action, suitable for
346
+ # use in output logging
347
+ # @yield perform optional work just after action has complted
348
+ # @return [self] this instance, used to chain actions
349
+ # @api private
271
350
  def perform_action(verb, output_verb)
272
351
  banner "#{output_verb} #{to_str}..."
273
352
  elapsed = action(verb) { |state| driver.public_send(verb, state) }
274
- info("Finished #{output_verb.downcase} #{to_str}" +
353
+ info("Finished #{output_verb.downcase} #{to_str}" \
275
354
  " #{Util.duration(elapsed.real)}.")
276
355
  yield if block_given?
277
356
  self
278
357
  end
279
358
 
359
+ # Times a call to an action block and handles any raised exceptions. This
360
+ # method ensures that the last successfully completed action is persisted
361
+ # to the state file. The last action state will either be the desired
362
+ # action that is passed in or the previous action that was persisted to the
363
+ # state file.
364
+ #
365
+ # @param what [Symbol] the action to be performed
366
+ # @param block [Proc] a block to be called
367
+ # @return [Benchmark::Tms] timing information for the given action
368
+ # @raise [InstanceFailed] if a driver action fails to complete, signaled
369
+ # by a driver raising an ActionFailed exception. Typical reasons for this
370
+ # would be a driver create action failing, a chef convergence crashing
371
+ # in normal course of development, failing acceptance tests in the
372
+ # verify action, etc.
373
+ # @raise [ActionFailed] if an unforseen or unplanned exception is raised.
374
+ # This would usually indicate that a race condition was triggered, a
375
+ # bug exists in a driver, provisioner, or core, a transient IO error
376
+ # occured, etc.
377
+ # @api private
280
378
  def action(what, &block)
281
379
  state = state_file.read
282
380
  elapsed = Benchmark.measure do
@@ -287,9 +385,9 @@ module Kitchen
287
385
  rescue ActionFailed => e
288
386
  log_failure(what, e)
289
387
  raise(InstanceFailure, failure_message(what) +
290
- " Please see .kitchen/logs/#{self.name}.log for more details",
388
+ " Please see .kitchen/logs/#{name}.log for more details",
291
389
  e.backtrace)
292
- rescue Exception => e
390
+ rescue Exception => e # rubocop:disable Lint/RescueException
293
391
  log_failure(what, e)
294
392
  raise ActionFailed,
295
393
  "Failed to complete ##{what} action: [#{e.message}]", e.backtrace
@@ -297,6 +395,16 @@ module Kitchen
297
395
  state_file.write(state)
298
396
  end
299
397
 
398
+ # Runs a given action block through a common driver mutex if required or
399
+ # runs it directly otherwise. If a driver class' `.serial_actions` array
400
+ # includes the desired action, then the action must be run with a muxtex
401
+ # lock. Otherwise, it is assumed that the action can happen concurrently,
402
+ # or fully in parallel.
403
+ #
404
+ # @param what [Symbol] the action to be performed
405
+ # @param state [Hash] a mutable state hash for this instance
406
+ # @param block [Proc] a block to be called
407
+ # @api private
300
408
  def synchronize_or_call(what, state, &block)
301
409
  if Array(driver.class.serial_actions).include?(what)
302
410
  debug("#{to_str} is synchronizing on #{driver.class}##{what}")
@@ -309,11 +417,26 @@ module Kitchen
309
417
  end
310
418
  end
311
419
 
420
+ # Writes a high level message for logging and/or output.
421
+ #
422
+ # In this case, all instance banner messages will be written to the common
423
+ # Kitchen logger so that the high level flow of a run can be followed in
424
+ # the kitchen.log file.
425
+ #
426
+ # @api private
312
427
  def banner(*args)
313
428
  Kitchen.logger.logdev && Kitchen.logger.logdev.banner(*args)
314
429
  super
315
430
  end
316
431
 
432
+ # Logs a failure (message and backtrace) to the instance's file logger
433
+ # to help with debugging and diagnosing issues without overwhelming the
434
+ # console output in the default case (i.e. running kitchen with :info
435
+ # level debugging).
436
+ #
437
+ # @param what [String] an action
438
+ # @param e [Exception] an exception
439
+ # @api private
317
440
  def log_failure(what, e)
318
441
  return if logger.logdev.nil?
319
442
 
@@ -321,13 +444,20 @@ module Kitchen
321
444
  Error.formatted_trace(e).each { |line| logger.logdev.error(line) }
322
445
  end
323
446
 
447
+ # Returns a string explaining what action failed, at a high level. Used
448
+ # for displaying to end user.
449
+ #
450
+ # @param what [String] an action
451
+ # @return [String] a failure message
452
+ # @api private
324
453
  def failure_message(what)
325
- "#{what.capitalize} failed on instance #{self.to_str}."
454
+ "#{what.capitalize} failed on instance #{to_str}."
326
455
  end
327
456
 
328
457
  # The simplest finite state machine pseudo-implementation needed to manage
329
458
  # an Instance.
330
459
  #
460
+ # @api private
331
461
  # @author Fletcher Nichol <fnichol@nichol.ca>
332
462
  class FSM
333
463
 
@@ -339,6 +469,7 @@ module Kitchen
339
469
  # @param desired [String,Symbol] the desired transitioned state for the
340
470
  # Instance
341
471
  # @return [Array<Symbol>] an Array of transition actions to perform
472
+ # @api private
342
473
  def self.actions(last = nil, desired)
343
474
  last_index = index(last)
344
475
  desired_index = index(desired)
@@ -350,10 +481,13 @@ module Kitchen
350
481
  end
351
482
  end
352
483
 
353
- private
354
-
355
484
  TRANSITIONS = [:destroy, :create, :converge, :setup, :verify]
356
485
 
486
+ # Determines the index of a state in the state lifecycle vector. Woah.
487
+ #
488
+ # @param transition [Symbol,#to_sym] a state
489
+ # @param [Integer] the index position
490
+ # @api private
357
491
  def self.index(transition)
358
492
  if transition.nil?
359
493
  0