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

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -16,6 +16,8 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
+ require 'kitchen/lazy_hash'
20
+
19
21
  module Kitchen
20
22
 
21
23
  module Provisioner
@@ -27,10 +29,35 @@ module Kitchen
27
29
 
28
30
  include Logging
29
31
 
30
- def initialize(instance, config)
31
- @instance = instance
32
- @config = config
33
- @logger = instance.logger
32
+ attr_accessor :instance
33
+
34
+ def initialize(config = {})
35
+ @config = LazyHash.new(config, self)
36
+ self.class.defaults.each do |attr, value|
37
+ @config[attr] = value unless @config.has_key?(attr)
38
+ end
39
+ end
40
+
41
+ # Returns the name of this driver, suitable for display in a CLI.
42
+ #
43
+ # @return [String] name of this driver
44
+ def name
45
+ self.class.name.split('::').last
46
+ end
47
+
48
+ # Provides hash-like access to configuration keys.
49
+ #
50
+ # @param attr [Object] configuration key
51
+ # @return [Object] value at configuration key
52
+ def [](attr)
53
+ config[attr]
54
+ end
55
+
56
+ # Returns an array of configuration keys.
57
+ #
58
+ # @return [Array] array of configuration keys
59
+ def config_keys
60
+ config.keys
34
61
  end
35
62
 
36
63
  def install_command ; end
@@ -45,19 +72,38 @@ module Kitchen
45
72
 
46
73
  def cleanup_sandbox ; end
47
74
 
48
- def home_path ; end
49
-
50
75
  protected
51
76
 
52
- attr_reader :instance, :logger, :config, :tmpdir
77
+ attr_reader :config
78
+
79
+ def logger
80
+ instance ? instance.logger : Kitchen.logger
81
+ end
53
82
 
54
83
  def sudo(script)
55
84
  config[:sudo] ? "sudo -E #{script}" : script
56
85
  end
57
86
 
58
- def kitchen_root
59
- config[:kitchen_root]
87
+ def self.defaults
88
+ @defaults ||= Hash.new.merge(super_defaults)
89
+ end
90
+
91
+ def self.super_defaults
92
+ klass = self.superclass
93
+
94
+ if klass.respond_to?(:defaults)
95
+ klass.defaults
96
+ else
97
+ Hash.new
98
+ end
99
+ end
100
+
101
+ def self.default_config(attr, value = nil, &block)
102
+ defaults[attr] = block_given? ? block : value
60
103
  end
104
+
105
+ default_config :root_path, "/tmp/kitchen"
106
+ default_config :sudo, true
61
107
  end
62
108
  end
63
109
  end
@@ -0,0 +1,76 @@
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 'kitchen/errors'
20
+ require 'kitchen/logging'
21
+
22
+ module Kitchen
23
+
24
+ module Provisioner
25
+
26
+ module Chef
27
+
28
+ # Chef cookbook resolver that uses Berkshelf and a Berksfile to calculate
29
+ # dependencies.
30
+ #
31
+ # @author Fletcher Nichol <fnichol@nichol.ca>
32
+ class Berkshelf
33
+
34
+ include Logging
35
+
36
+ def initialize(berksfile, path, logger = Kitchen.logger)
37
+ @berksfile = berksfile
38
+ @path = path
39
+ @logger = logger
40
+ end
41
+
42
+ def resolve
43
+ info("Resolving cookbook dependencies with Berkshelf...")
44
+ debug("Using Berksfile from #{berksfile}")
45
+
46
+ load_berkshelf!
47
+
48
+ ::Berkshelf.ui.mute do
49
+ if ::Berkshelf::Berksfile.method_defined?(:vendor)
50
+ # Berkshelf 3.0 requires the directory to not exist
51
+ FileUtils.rm_rf(path)
52
+ ::Berkshelf::Berksfile.from_file(berksfile).vendor(path)
53
+ else
54
+ ::Berkshelf::Berksfile.from_file(berksfile).install(path: path)
55
+ end
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :berksfile, :path, :logger
62
+
63
+ def load_berkshelf!
64
+ require 'berkshelf'
65
+ rescue LoadError => e
66
+ fatal("The `berkshelf' gem is missing and must be installed" +
67
+ " or cannot be properly activated. Run" +
68
+ " `gem install berkshelf` or add the following to your" +
69
+ " Gemfile if you are using Bundler: `gem 'berkshelf'`.")
70
+ raise UserError,
71
+ "Could not load or activate Berkshelf (#{e.message})"
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,72 @@
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 'kitchen/errors'
20
+ require 'kitchen/logging'
21
+
22
+ module Kitchen
23
+
24
+ module Provisioner
25
+
26
+ module Chef
27
+
28
+ # Chef cookbook resolver that uses Librarian-Chef and a Cheffile to
29
+ # calculate # dependencies.
30
+ #
31
+ # @author Fletcher Nichol <fnichol@nichol.ca>
32
+ class Librarian
33
+
34
+ include Logging
35
+
36
+ def initialize(cheffile, path, logger = Kitchen.logger)
37
+ @cheffile = cheffile
38
+ @path = path
39
+ @logger = logger
40
+ end
41
+
42
+ def resolve
43
+ info("Resolving cookbook dependencies with Librarian-Chef")
44
+ debug("Using Cheffile from #{cheffile}")
45
+
46
+ load_librarian!
47
+
48
+ env = ::Librarian::Chef::Environment.new(
49
+ :project_path => File.dirname(cheffile))
50
+ env.config_db.local["path"] = path
51
+ ::Librarian::Action::Resolve.new(env).run
52
+ ::Librarian::Action::Install.new(env).run
53
+ end
54
+
55
+ attr_reader :cheffile, :path, :logger
56
+
57
+ def load_librarian!
58
+ require 'librarian/chef/environment'
59
+ require 'librarian/action/resolve'
60
+ require 'librarian/action/install'
61
+ rescue LoadError => e
62
+ fatal("The `librarian-chef' gem is missing and must be installed" +
63
+ " or cannot be properly activated. Run" +
64
+ " `gem install librarian-chef` or add the following to your" +
65
+ " Gemfile if you are using Bundler: `gem 'librarian-chef'`.")
66
+ raise UserError,
67
+ "Could not load or activate Librarian-Chef (#{e.message})"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -16,10 +16,12 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
- require 'buff/ignore'
20
19
  require 'fileutils'
21
20
  require 'pathname'
22
21
  require 'json'
22
+
23
+ require 'kitchen/provisioner/chef/berkshelf'
24
+ require 'kitchen/provisioner/chef/librarian'
23
25
  require 'kitchen/util'
24
26
 
25
27
  module Kitchen
@@ -31,10 +33,47 @@ module Kitchen
31
33
  # @author Fletcher Nichol <fnichol@nichol.ca>
32
34
  class ChefBase < Base
33
35
 
36
+ default_config :require_chef_omnibus, true
37
+ default_config :chef_omnibus_url, "https://www.opscode.com/chef/install.sh"
38
+ default_config :run_list, []
39
+ default_config :attributes, {}
40
+ default_config :cookbook_files_glob, %w[README.* metadata.{json,rb}
41
+ attributes/**/* definitions/**/* files/**/* libraries/**/*
42
+ providers/**/* recipes/**/* resources/**/* templates/**/*].join(",")
43
+
44
+ default_config :data_path do |provisioner|
45
+ provisioner.calculate_path("data")
46
+ end
47
+
48
+ default_config :data_bags_path do |provisioner|
49
+ provisioner.calculate_path("data_bags")
50
+ end
51
+
52
+ default_config :environments_path do |provisioner|
53
+ provisioner.calculate_path("environments")
54
+ end
55
+
56
+ default_config :nodes_path do |provisioner|
57
+ provisioner.calculate_path("nodes")
58
+ end
59
+
60
+ default_config :roles_path do |provisioner|
61
+ provisioner.calculate_path("roles")
62
+ end
63
+
64
+ default_config :encrypted_data_bag_secret_key_path do |provisioner|
65
+ provisioner.calculate_path("encrypted_data_bag_secret", :file)
66
+ end
67
+
68
+ def instance=(instance)
69
+ @instance = instance
70
+ expand_paths!
71
+ end
72
+
34
73
  def install_command
35
- return nil unless config[:require_chef_omnibus]
74
+ return unless config[:require_chef_omnibus]
36
75
 
37
- url = config[:chef_omnibus_url] || "https://www.opscode.com/chef/install.sh"
76
+ url = config[:chef_omnibus_url]
38
77
  flag = config[:require_chef_omnibus]
39
78
  version = if flag.is_a?(String) && flag != "latest"
40
79
  "-v #{flag.downcase}"
@@ -63,7 +102,9 @@ module Kitchen
63
102
  end
64
103
 
65
104
  def init_command
66
- "#{sudo('rm')} -rf #{home_path}"
105
+ dirs = %w{cookbooks data data_bags environments roles}.
106
+ map { |dir| File.join(config[:root_path], dir) }.join(" ")
107
+ "#{sudo('rm')} -rf #{dirs} ; mkdir -p #{config[:root_path]}"
67
108
  end
68
109
 
69
110
  def cleanup_sandbox
@@ -73,8 +114,59 @@ module Kitchen
73
114
  FileUtils.rmtree(tmpdir)
74
115
  end
75
116
 
117
+ def calculate_path(path, type = :directory)
118
+ base = config[:test_base_path]
119
+ candidates = []
120
+ candidates << File.join(base, instance.suite.name, path)
121
+ candidates << File.join(base, path)
122
+ candidates << File.join(Dir.pwd, path)
123
+
124
+ candidates.find do |c|
125
+ type == :directory ? File.directory?(c) : File.file?(c)
126
+ end
127
+ end
128
+
76
129
  protected
77
130
 
131
+ attr_reader :tmpdir
132
+
133
+ def expand_paths!
134
+ paths = %w{test_base data data_bags environments nodes roles}
135
+ paths.map{ |p| "#{p}_path".to_sym }.each do |key|
136
+ unless config[key].nil?
137
+ config[key] = File.expand_path(config[key], config[:kitchen_root])
138
+ end
139
+ end
140
+ end
141
+
142
+ def format_config_file(data)
143
+ data.each.map { |attr, value|
144
+ [attr, (value.is_a?(Array) ? value.to_s : %{"#{value}"})].join(" ")
145
+ }.join("\n")
146
+ end
147
+
148
+ def default_config_rb
149
+ root = config[:root_path]
150
+
151
+ {
152
+ :node_name => instance.name,
153
+ :checksum_path => "#{root}/checksums",
154
+ :file_cache_path => "#{root}/cache",
155
+ :file_backup_path => "#{root}/backup",
156
+ :cookbook_path => ["#{root}/cookbooks", "#{root}/site-cookbooks"],
157
+ :data_bag_path => "#{root}/data_bags",
158
+ :environment_path => "#{root}/environments",
159
+ :node_path => "#{root}/nodes",
160
+ :role_path => "#{root}/roles",
161
+ :client_path => "#{root}/clients",
162
+ :user_path => "#{root}/users",
163
+ :validation_key => "#{root}/validation.pem",
164
+ :client_key => "#{root}/client.pem",
165
+ :chef_server_url => "http://127.0.0.1:8889",
166
+ :encrypted_data_bag_secret => "#{root}/encrypted_data_bag_secret",
167
+ }
168
+ end
169
+
78
170
  def create_chef_sandbox
79
171
  @tmpdir = Dir.mktmpdir("#{instance.name}-sandbox-")
80
172
  File.chmod(0755, @tmpdir)
@@ -82,22 +174,36 @@ module Kitchen
82
174
 
83
175
  yield if block_given?
84
176
  prepare_json
177
+ prepare_cache
178
+ prepare_cookbooks
179
+ prepare_data
85
180
  prepare_data_bags
86
- prepare_roles
87
- prepare_nodes
88
181
  prepare_environments
182
+ prepare_nodes
183
+ prepare_roles
89
184
  prepare_secret
90
- prepare_cache
91
- prepare_cookbooks
92
185
  tmpdir
93
186
  end
94
187
 
95
188
  def prepare_json
189
+ dna = config[:attributes].merge({ :run_list => config[:run_list] })
190
+
96
191
  File.open(File.join(tmpdir, "dna.json"), "wb") do |file|
97
- file.write(instance.dna.to_json)
192
+ file.write(dna.to_json)
98
193
  end
99
194
  end
100
195
 
196
+ def prepare_data
197
+ return unless data
198
+
199
+ info("Preparing data")
200
+ debug("Using data from #{data}")
201
+
202
+ tmpdata_dir = File.join(tmpdir, "data")
203
+ FileUtils.mkdir_p(tmpdata_dir)
204
+ FileUtils.cp_r(Dir.glob("#{data}/*"), tmpdata_dir)
205
+ end
206
+
101
207
  def prepare_data_bags
102
208
  return unless data_bags
103
209
 
@@ -165,65 +271,71 @@ module Kitchen
165
271
  elsif File.exists?(metadata_rb)
166
272
  cp_this_cookbook
167
273
  else
168
- FileUtils.rmtree(tmpdir)
169
- fatal("Berksfile, Cheffile, cookbooks/, or metadata.rb" +
170
- " must exist in #{kitchen_root}")
171
- raise UserError, "Cookbooks could not be found"
274
+ make_fake_cookbook
172
275
  end
173
276
 
174
- remove_ignored_files
277
+ filter_only_cookbook_files
175
278
  end
176
279
 
177
- def remove_ignored_files
178
- cookbooks_in_tmpdir do |cookbook_path|
179
- chefignore = File.join(cookbook_path, "chefignore")
180
- if File.exist? chefignore
181
- ignores = Buff::Ignore::IgnoreFile.new(chefignore)
182
- cookbook_files = Dir.glob(File.join(cookbook_path, "**/*"), File::FNM_DOTMATCH).
183
- select { |fn| File.file?(fn) && fn != '.' && fn != '..' }
184
- cookbook_files.each { |file| FileUtils.rm(file) if ignores.ignored?(file) }
185
- end
186
- end
280
+ def filter_only_cookbook_files
281
+ info("Removing non-cookbook files in sandbox")
282
+ FileUtils.rm(all_files_in_cookbooks - only_cookbook_files)
283
+ end
284
+
285
+ def all_files_in_cookbooks
286
+ Dir.glob(File.join(tmpbooks_dir, "**/*"), File::FNM_DOTMATCH).
287
+ select { |fn| File.file?(fn) && ! %w{. ..}.include?(fn) }
288
+ end
289
+
290
+ def only_cookbook_files
291
+ glob = File.join(tmpbooks_dir, "*", "{#{config[:cookbook_files_glob]}}")
292
+
293
+ Dir.glob(glob, File::FNM_DOTMATCH).
294
+ select { |fn| File.file?(fn) && ! %w{. ..}.include?(fn) }
187
295
  end
188
296
 
189
297
  def berksfile
190
- File.join(kitchen_root, "Berksfile")
298
+ File.join(config[:kitchen_root], "Berksfile")
191
299
  end
192
300
 
193
301
  def cheffile
194
- File.join(kitchen_root, "Cheffile")
302
+ File.join(config[:kitchen_root], "Cheffile")
195
303
  end
196
304
 
197
305
  def metadata_rb
198
- File.join(kitchen_root, "metadata.rb")
306
+ File.join(config[:kitchen_root], "metadata.rb")
199
307
  end
200
308
 
201
309
  def cookbooks_dir
202
- File.join(kitchen_root, "cookbooks")
310
+ File.join(config[:kitchen_root], "cookbooks")
203
311
  end
204
312
 
205
313
  def site_cookbooks_dir
206
- File.join(kitchen_root, "site-cookbooks")
314
+ File.join(config[:kitchen_root], "site-cookbooks")
207
315
  end
208
316
 
209
317
  def data_bags
210
- instance.suite.data_bags_path
318
+ config[:data_bags_path]
211
319
  end
212
320
 
213
321
  def roles
214
- instance.suite.roles_path
322
+ config[:roles_path]
215
323
  end
216
324
 
217
325
  def nodes
218
- instance.suite.nodes_path
326
+ config[:nodes_path]
327
+ end
328
+
329
+ def data
330
+ config[:data_path]
219
331
  end
220
332
 
221
333
  def environments
222
- instance.suite.environments_path
334
+ config[:environments_path]
223
335
  end
224
336
 
225
337
  def secret
226
- instance.suite.encrypted_data_bag_secret_key_path
338
+ config[:encrypted_data_bag_secret_key_path]
227
339
  end
228
340
 
229
341
  def tmpbooks_dir
@@ -260,63 +372,32 @@ module Kitchen
260
372
 
261
373
  cb_path = File.join(tmpbooks_dir, cb_name)
262
374
 
263
- glob = Dir.glob("#{kitchen_root}/**")
375
+ glob = Dir.glob("#{config[:kitchen_root]}/**")
264
376
 
265
377
  FileUtils.mkdir_p(cb_path)
266
378
  FileUtils.cp_r(glob, cb_path)
267
379
  end
268
380
 
269
- def resolve_with_berkshelf
270
- info("Resolving cookbook dependencies with Berkshelf")
271
- debug("Using Berksfile from #{berksfile}")
272
-
273
- begin
274
- require 'berkshelf'
275
- rescue LoadError => e
276
- fatal("The `berkshelf' gem is missing and must be installed" +
277
- " or cannot be properly activated. Run" +
278
- " `gem install berkshelf` or add the following to your" +
279
- " Gemfile if you are using Bundler: `gem 'berkshelf'`.")
280
- raise UserError,
281
- "Could not load or activate Berkshelf (#{e.message})"
381
+ def make_fake_cookbook
382
+ info("Berksfile, Cheffile, cookbooks/, or metadata.rb not found " +
383
+ "so Chef will run with effectively no cookbooks. Is this intended?")
384
+ name = File.basename(config[:kitchen_root])
385
+ fake_cb = File.join(tmpbooks_dir, name)
386
+ FileUtils.mkdir_p(fake_cb)
387
+ File.open(File.join(fake_cb, "metadata.rb"), "wb") do |file|
388
+ file.write(%{name "#{name}\n"})
282
389
  end
390
+ end
283
391
 
392
+ def resolve_with_berkshelf
284
393
  Kitchen.mutex.synchronize do
285
- Berkshelf::Berksfile.from_file(berksfile).
286
- install(:path => tmpbooks_dir)
394
+ Chef::Berkshelf.new(berksfile, tmpbooks_dir, logger).resolve
287
395
  end
288
396
  end
289
397
 
290
398
  def resolve_with_librarian
291
- info("Resolving cookbook dependencies with Librarian-Chef")
292
- debug("Using Cheffile from #{cheffile}")
293
-
294
- begin
295
- require 'librarian/chef/environment'
296
- require 'librarian/action/resolve'
297
- require 'librarian/action/install'
298
- rescue LoadError => e
299
- fatal("The `librarian-chef' gem is missing and must be installed" +
300
- " or cannot be properly activated. Run" +
301
- " `gem install librarian-chef` or add the following to your" +
302
- " Gemfile if you are using Bundler: `gem 'librarian-chef'`.")
303
- raise UserError,
304
- "Could not load or activate Librarian-Chef (#{e.message})"
305
- end
306
-
307
399
  Kitchen.mutex.synchronize do
308
- env = Librarian::Chef::Environment.new(:project_path => kitchen_root)
309
- env.config_db.local["path"] = tmpbooks_dir
310
- Librarian::Action::Resolve.new(env).run
311
- Librarian::Action::Install.new(env).run
312
- end
313
- end
314
-
315
- private
316
-
317
- def cookbooks_in_tmpdir
318
- Dir.glob(File.join(tmpbooks_dir, "*/")).each do |cookbook_path|
319
- yield cookbook_path if block_given?
400
+ Chef::Librarian.new(cheffile, tmpbooks_dir, logger).resolve
320
401
  end
321
402
  end
322
403
  end