test-kitchen 1.0.0.beta.4 → 1.0.0.rc.1

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -1
  3. data/Gemfile +1 -1
  4. data/README.md +18 -7
  5. data/Rakefile +8 -1
  6. data/features/kitchen_init_command.feature +90 -11
  7. data/features/step_definitions/git_steps.rb +3 -0
  8. data/lib/kitchen/busser.rb +79 -45
  9. data/lib/kitchen/cli.rb +14 -13
  10. data/lib/kitchen/config.rb +79 -138
  11. data/lib/kitchen/data_munger.rb +224 -0
  12. data/lib/kitchen/driver/base.rb +4 -31
  13. data/lib/kitchen/driver/ssh_base.rb +6 -16
  14. data/lib/kitchen/driver.rb +4 -0
  15. data/lib/kitchen/generator/init.rb +20 -9
  16. data/lib/kitchen/instance.rb +53 -58
  17. data/lib/kitchen/lazy_hash.rb +50 -0
  18. data/lib/kitchen/platform.rb +2 -31
  19. data/lib/kitchen/provisioner/base.rb +55 -9
  20. data/lib/kitchen/provisioner/chef/berkshelf.rb +76 -0
  21. data/lib/kitchen/provisioner/chef/librarian.rb +72 -0
  22. data/lib/kitchen/provisioner/chef_base.rb +159 -78
  23. data/lib/kitchen/provisioner/chef_solo.rb +6 -36
  24. data/lib/kitchen/provisioner/chef_zero.rb +70 -59
  25. data/lib/kitchen/provisioner/dummy.rb +28 -0
  26. data/lib/kitchen/provisioner.rb +6 -4
  27. data/lib/kitchen/shell_out.rb +2 -5
  28. data/lib/kitchen/ssh.rb +1 -1
  29. data/lib/kitchen/suite.rb +10 -79
  30. data/lib/kitchen/util.rb +2 -2
  31. data/lib/kitchen/version.rb +2 -2
  32. data/lib/kitchen.rb +5 -0
  33. data/spec/kitchen/config_spec.rb +84 -123
  34. data/spec/kitchen/data_munger_spec.rb +1412 -0
  35. data/spec/kitchen/driver/base_spec.rb +30 -0
  36. data/spec/kitchen/instance_spec.rb +868 -86
  37. data/spec/kitchen/lazy_hash_spec.rb +63 -0
  38. data/spec/kitchen/platform_spec.rb +0 -22
  39. data/spec/kitchen/provisioner/base_spec.rb +210 -0
  40. data/spec/kitchen/provisioner_spec.rb +70 -0
  41. data/spec/kitchen/suite_spec.rb +25 -38
  42. data/spec/spec_helper.rb +1 -0
  43. data/support/chef-client-zero.rb +51 -35
  44. data/support/dummy-validation.pem +27 -0
  45. data/templates/init/kitchen.yml.erb +10 -22
  46. data/test-kitchen.gemspec +1 -2
  47. metadata +20 -18
@@ -18,6 +18,8 @@
18
18
 
19
19
  require 'thor/util'
20
20
 
21
+ require 'kitchen/lazy_hash'
22
+
21
23
  module Kitchen
22
24
 
23
25
  module Driver
@@ -37,7 +39,7 @@ module Kitchen
37
39
  end
38
40
 
39
41
  def initialize(config = {})
40
- @config = LazyDriverHash.new(config, self)
42
+ @config = LazyHash.new(config, self)
41
43
  self.class.defaults.each do |attr, value|
42
44
  @config[attr] = value unless @config.has_key?(attr)
43
45
  end
@@ -162,8 +164,7 @@ module Kitchen
162
164
  def busser
163
165
  @busser ||= begin
164
166
  raise ClientError, "Instance must be set for Driver" if instance.nil?
165
-
166
- Busser.new(instance.suite.name, config)
167
+ instance.busser
167
168
  end
168
169
  end
169
170
 
@@ -213,34 +214,6 @@ module Kitchen
213
214
  @serial_actions ||= []
214
215
  @serial_actions += methods
215
216
  end
216
-
217
- # A modifed Hash object that may contain procs as a value which must be
218
- # executed in the context of a Driver object.
219
- #
220
- # @author Fletcher Nichol <fnichol@nichol.ca>
221
- class LazyDriverHash < SimpleDelegator
222
-
223
- def initialize(obj, driver)
224
- @driver = driver
225
- super(obj)
226
- end
227
-
228
- def [](key)
229
- proc_or_val = __getobj__[key]
230
-
231
- if proc_or_val.respond_to?(:call)
232
- proc_or_val.call(@driver)
233
- else
234
- proc_or_val
235
- end
236
- end
237
-
238
- def to_hash
239
- hash = Hash.new
240
- __getobj__.keys.each { |key| hash[key] = self[key] }
241
- hash
242
- end
243
- end
244
217
  end
245
218
  end
246
219
  end
@@ -36,12 +36,13 @@ module Kitchen
36
36
  end
37
37
 
38
38
  def converge(state)
39
- provisioner = new_provisioner
39
+ provisioner = instance.provisioner
40
+ sandbox_dirs = Dir.glob("#{provisioner.create_sandbox}/*")
40
41
 
41
42
  Kitchen::SSH.new(*build_ssh_args(state)) do |conn|
42
43
  run_remote(provisioner.install_command, conn)
43
44
  run_remote(provisioner.init_command, conn)
44
- transfer_path(provisioner.create_sandbox, provisioner.home_path, conn)
45
+ transfer_path(sandbox_dirs, provisioner[:root_path], conn)
45
46
  run_remote(provisioner.prepare_command, conn)
46
47
  run_remote(provisioner.run_command, conn)
47
48
  end
@@ -78,12 +79,6 @@ module Kitchen
78
79
 
79
80
  protected
80
81
 
81
- def new_provisioner
82
- combined = config.dup
83
- combined[:log_level] = Util.from_logger_level(logger.level)
84
- Provisioner.for_plugin(combined[:provisioner], instance, combined)
85
- end
86
-
87
82
  def build_ssh_args(state)
88
83
  combined = config.to_hash.merge(state)
89
84
 
@@ -104,11 +99,6 @@ module Kitchen
104
99
  env << " http_proxy=#{config[:http_proxy]}" if config[:http_proxy]
105
100
  env << " https_proxy=#{config[:https_proxy]}" if config[:https_proxy]
106
101
 
107
- additional_paths = []
108
- additional_paths << config[:ruby_binpath] if config[:ruby_binpath]
109
- additional_paths << config[:path] if config[:path]
110
- env << " PATH=$PATH:#{additional_paths.join(':')}" if additional_paths.any?
111
-
112
102
  env == "env" ? cmd : "#{env} #{cmd}"
113
103
  end
114
104
 
@@ -120,10 +110,10 @@ module Kitchen
120
110
  raise ActionFailed, ex.message
121
111
  end
122
112
 
123
- def transfer_path(local, remote, connection)
124
- return if local.nil?
113
+ def transfer_path(locals, remote, connection)
114
+ return if locals.nil? || Array(locals).empty?
125
115
 
126
- connection.upload_path!(local, remote)
116
+ locals.each { |local| connection.upload_path!(local, remote) }
127
117
  rescue SSHFailed, Net::SSH::Exception => ex
128
118
  raise ActionFailed, ex.message
129
119
  end
@@ -26,9 +26,13 @@ module Kitchen
26
26
  # @author Fletcher Nichol <fnichol@nichol.ca>
27
27
  module Driver
28
28
 
29
+ # Default driver plugin to use
30
+ DEFAULT_PLUGIN = "dummy".freeze
31
+
29
32
  # Returns an instance of a driver given a plugin type string.
30
33
  #
31
34
  # @param plugin [String] a driver plugin type, which will be constantized
35
+ # @param config [Hash] a configuration hash to initialize the driver
32
36
  # @return [Driver::Base] a driver instance
33
37
  # @raise [ClientError] if a driver instance could not be created
34
38
  # @raise [UserError] if the driver's dependencies could not be met
@@ -37,6 +37,12 @@ module Kitchen
37
37
  Gemfile
38
38
  D
39
39
 
40
+ class_option :provisioner, :type => :string, :aliases => "-P",
41
+ :default => "chef_solo",
42
+ :desc => <<-D.gsub(/^\s+/, '').gsub(/\n/, ' ')
43
+ The default Kitchen Provisioner to use
44
+ D
45
+
40
46
  class_option :create_gemfile, :type => :boolean, :default => false,
41
47
  :desc => <<-D.gsub(/^\s+/, '').gsub(/\n/, ' ')
42
48
  Whether or not to create a Gemfile if one does not exist.
@@ -50,8 +56,10 @@ module Kitchen
50
56
  prepare_rakefile if init_rakefile?
51
57
  prepare_thorfile if init_thorfile?
52
58
  empty_directory "test/integration/default" if init_test_dir?
53
- append_to_gitignore(".kitchen/")
54
- append_to_gitignore(".kitchen.local.yml")
59
+ if init_git?
60
+ append_to_gitignore(".kitchen/")
61
+ append_to_gitignore(".kitchen.local.yml")
62
+ end
55
63
  prepare_gemfile if init_gemfile?
56
64
  add_drivers
57
65
 
@@ -68,11 +76,12 @@ module Kitchen
68
76
  else
69
77
  nil
70
78
  end
71
- run_list = cookbook_name ? "recipe[#{cookbook_name}]" : nil
79
+ run_list = cookbook_name ? "recipe[#{cookbook_name}::default]" : nil
72
80
  driver_plugin = Array(options[:driver]).first || 'dummy'
73
81
 
74
82
  template("kitchen.yml.erb", ".kitchen.yml", {
75
83
  :driver_plugin => driver_plugin.sub(/^kitchen-/, ''),
84
+ :provisioner => options[:provisioner],
76
85
  :run_list => Array(run_list)
77
86
  })
78
87
  end
@@ -96,6 +105,10 @@ module Kitchen
96
105
  Dir.glob("test/integration/*").select { |d| File.directory?(d) }.empty?
97
106
  end
98
107
 
108
+ def init_git?
109
+ File.directory?(File.join(destination_root, '.git'))
110
+ end
111
+
99
112
  def prepare_rakefile
100
113
  rakedoc = <<-RAKE.gsub(/^ {10}/, '')
101
114
 
@@ -142,9 +155,8 @@ module Kitchen
142
155
  end
143
156
 
144
157
  def add_gem_to_gemfile
145
- if not_in_file?("Gemfile", %r{gem 'test-kitchen'})
146
- append_to_file("Gemfile",
147
- %{gem 'test-kitchen', :group => :integration\n})
158
+ if not_in_file?("Gemfile", %r{gem ('|")test-kitchen('|")})
159
+ append_to_file("Gemfile", %{gem 'test-kitchen'\n})
148
160
  @display_bundle_msg = true
149
161
  end
150
162
  end
@@ -163,9 +175,8 @@ module Kitchen
163
175
  end
164
176
 
165
177
  def add_driver_to_gemfile(driver_gem)
166
- if not_in_file?("Gemfile", %r{gem '#{driver_gem}'})
167
- append_to_file("Gemfile",
168
- %{gem '#{driver_gem}', :group => :integration\n})
178
+ if not_in_file?("Gemfile", %r{gem ('|")#{driver_gem}('|")})
179
+ append_to_file("Gemfile", %{gem '#{driver_gem}'\n})
169
180
  @display_bundle_msg = true
170
181
  end
171
182
  end
@@ -18,7 +18,6 @@
18
18
 
19
19
  require 'benchmark'
20
20
  require 'fileutils'
21
- require 'vendor/hash_recursive_merge'
22
21
 
23
22
  module Kitchen
24
23
 
@@ -33,6 +32,10 @@ module Kitchen
33
32
 
34
33
  class << self
35
34
  attr_accessor :mutexes
35
+
36
+ def name_for(suite, platform)
37
+ "#{suite.name}-#{platform.name}".gsub(/_/, '-').gsub(/\./, '')
38
+ end
36
39
  end
37
40
 
38
41
  # @return [Suite] the test suite configuration
@@ -41,38 +44,53 @@ module Kitchen
41
44
  # @return [Platform] the target platform configuration
42
45
  attr_reader :platform
43
46
 
47
+ # @return [String] name of this instance
48
+ attr_reader :name
49
+
44
50
  # @return [Driver::Base] driver object which will manage this instance's
45
51
  # lifecycle actions
46
52
  attr_reader :driver
47
53
 
54
+ # @return [Provisioner::Base] provisioner object which will the setup
55
+ # and invocation instructions for configuration management and other
56
+ # automation tools
57
+ attr_reader :provisioner
58
+
59
+ # @return [Busser] busser object for instance to manage the busser
60
+ # installation on this instance
61
+ attr_reader :busser
62
+
48
63
  # @return [Logger] the logger for this instance
49
64
  attr_reader :logger
50
65
 
51
66
  # Creates a new instance, given a suite and a platform.
52
67
  #
53
68
  # @param [Hash] options configuration for a new suite
54
- # @option options [Suite] :suite the suite
55
- # @option options [Platform] :platform the platform
56
- # @option options [Driver::Base] :driver the driver
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)
72
+ # @option options [Provisioner::Base] :provisioner the provisioner
73
+ # (**Required)
74
+ # @option options [Busser] :busser the busser logger (**Required**)
57
75
  # @option options [Logger] :logger the instance logger
76
+ # (default: Kitchen.logger)
77
+ # @option options [StateFile] :state_file the state file object to use
78
+ # when tracking instance state (**Required**)
79
+ # @raise [ClientError] if one or more required options are omitted
58
80
  def initialize(options = {})
59
- options = { :logger => Kitchen.logger }.merge(options)
60
81
  validate_options(options)
61
- logger = options[:logger]
62
-
63
- @suite = options[:suite]
64
- @platform = options[:platform]
65
- @driver = options[:driver]
66
- @logger = logger.is_a?(Proc) ? logger.call(name) : logger
67
82
 
68
- @driver.instance = self
69
- @driver.validate_config!
70
- setup_driver_mutex
71
- end
72
-
73
- # @return [String] name of this instance
74
- def name
75
- "#{suite.name}-#{platform.name}".gsub(/_/, '-').gsub(/\./, '')
83
+ @suite = options.fetch(:suite)
84
+ @platform = options.fetch(:platform)
85
+ @name = self.class.name_for(@suite, @platform)
86
+ @driver = options.fetch(:driver)
87
+ @provisioner = options.fetch(:provisioner)
88
+ @busser = options.fetch(:busser)
89
+ @logger = options.fetch(:logger) { Kitchen.logger }
90
+ @state_file = options.fetch(:state_file)
91
+
92
+ setup_driver
93
+ setup_provisioner
76
94
  end
77
95
 
78
96
  def to_str
@@ -179,45 +197,22 @@ module Kitchen
179
197
  state_file.read[:last_action]
180
198
  end
181
199
 
182
- # Extra instance methods used for accessing Puppet data such as a combined
183
- # run list, node attributes, etc.
184
- module Cheflike
185
-
186
- # Returns a combined run_list starting with the platform's run_list
187
- # followed by the suite's run_list.
188
- #
189
- # @return [Array] combined run_list from suite and platform
190
- def run_list
191
- Array(platform.run_list) + Array(suite.run_list)
192
- end
193
-
194
- # Returns a merged hash of Chef node attributes with values from the
195
- # suite overriding values from the platform.
196
- #
197
- # @return [Hash] merged hash of Chef node attributes
198
- def attributes
199
- platform.attributes.rmerge(suite.attributes)
200
- end
201
-
202
- def dna
203
- attributes.rmerge({ :run_list => run_list })
204
- end
205
- end
200
+ private
206
201
 
207
- # Extra instances methods used for accessing Puppet data such as a run,
208
- # node attributes, etc.
209
- module Puppetlike
202
+ attr_reader :state_file
210
203
 
211
- def manifest
204
+ def validate_options(options)
205
+ [:suite, :platform, :driver, :provisioner, :busser, :state_file].each do |k|
206
+ if !options.has_key?(k)
207
+ raise ClientError, "Instance#new requires option :#{k}"
208
+ end
212
209
  end
213
210
  end
214
211
 
215
- private
216
-
217
- def validate_options(opts)
218
- [:suite, :platform, :driver, :logger].each do |k|
219
- raise ClientError, "Instance#new requires option :#{k}" if opts[k].nil?
220
- end
212
+ def setup_driver
213
+ @driver.instance = self
214
+ @driver.validate_config!
215
+ setup_driver_mutex
221
216
  end
222
217
 
223
218
  def setup_driver_mutex
@@ -229,6 +224,10 @@ module Kitchen
229
224
  end
230
225
  end
231
226
 
227
+ def setup_provisioner
228
+ @provisioner.instance = self
229
+ end
230
+
232
231
  def transition_to(desired)
233
232
  result = nil
234
233
  FSM.actions(last_action, desired).each do |transition|
@@ -258,7 +257,7 @@ module Kitchen
258
257
  end
259
258
 
260
259
  def perform_action(verb, output_verb)
261
- banner "#{output_verb} #{to_str}"
260
+ banner "#{output_verb} #{to_str}..."
262
261
  elapsed = action(verb) { |state| driver.public_send(verb, state) }
263
262
  info("Finished #{output_verb.downcase} #{to_str}" +
264
263
  " #{Util.duration(elapsed.real)}.")
@@ -298,10 +297,6 @@ module Kitchen
298
297
  end
299
298
  end
300
299
 
301
- def state_file
302
- @state_file ||= StateFile.new(driver[:kitchen_root], name)
303
- end
304
-
305
300
  def banner(*args)
306
301
  Kitchen.logger.logdev && Kitchen.logger.logdev.banner(*args)
307
302
  super
@@ -0,0 +1,50 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2013, Fletcher Nichol
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require 'delegate'
20
+
21
+ module Kitchen
22
+
23
+ # A modifed Hash object that may contain procs as a value which must be
24
+ # executed in the context of another object.
25
+ #
26
+ # @author Fletcher Nichol <fnichol@nichol.ca>
27
+ class LazyHash < SimpleDelegator
28
+
29
+ def initialize(obj, context)
30
+ @context = context
31
+ super(obj)
32
+ end
33
+
34
+ def [](key)
35
+ proc_or_val = __getobj__[key]
36
+
37
+ if proc_or_val.respond_to?(:call)
38
+ proc_or_val.call(@context)
39
+ else
40
+ proc_or_val
41
+ end
42
+ end
43
+
44
+ def to_hash
45
+ hash = Hash.new
46
+ __getobj__.keys.each { |key| hash[key] = self[key] }
47
+ hash
48
+ end
49
+ end
50
+ end
@@ -34,37 +34,8 @@ module Kitchen
34
34
  # @option options [String] :name logical name of this platform
35
35
  # (**Required**)
36
36
  def initialize(options = {})
37
- options = options.dup
38
- validate_options(options)
39
-
40
- @name = options.delete(:name)
41
- @data = options.reject do |key, value|
42
- [:driver_config, :driver_plugin, :provisioner].include?(key)
43
- end
44
- end
45
-
46
- # Extra platform methods used for accessing Chef data such as a run list,
47
- # node attributes, etc.
48
- module Cheflike
49
-
50
- # @return [Array] Array of Chef run_list items
51
- def run_list
52
- Array(data[:run_list])
53
- end
54
-
55
- # @return [Hash] Hash of Chef node attributes
56
- def attributes
57
- data[:attributes] || Hash.new
58
- end
59
- end
60
-
61
- private
62
-
63
- attr_reader :data
64
-
65
- def validate_options(opts)
66
- [:name].each do |k|
67
- raise ClientError, "Platform#new requires option :#{k}" if opts[k].nil?
37
+ @name = options.fetch(:name) do
38
+ raise ClientError, "Platform#new requires option :name"
68
39
  end
69
40
  end
70
41
  end