test-kitchen 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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