test-kitchen 1.0.0.alpha.7 → 1.0.0.beta.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.
@@ -67,6 +67,7 @@ module Kitchen
67
67
  @logger = logger.is_a?(Proc) ? logger.call(name) : logger
68
68
 
69
69
  @driver.instance = self
70
+ @driver.validate_config!
70
71
  setup_driver_mutex
71
72
  end
72
73
 
@@ -79,26 +80,6 @@ module Kitchen
79
80
  "<#{name}>"
80
81
  end
81
82
 
82
- # Returns a combined run_list starting with the platform's run_list
83
- # followed by the suite's run_list.
84
- #
85
- # @return [Array] combined run_list from suite and platform
86
- def run_list
87
- Array(platform.run_list) + Array(suite.run_list)
88
- end
89
-
90
- # Returns a merged hash of Chef node attributes with values from the
91
- # suite overriding values from the platform.
92
- #
93
- # @return [Hash] merged hash of Chef node attributes
94
- def attributes
95
- platform.attributes.rmerge(suite.attributes)
96
- end
97
-
98
- def dna
99
- attributes.rmerge({ :run_list => run_list })
100
- end
101
-
102
83
  # Creates this instance.
103
84
  #
104
85
  # @see Driver::Base#create
@@ -199,6 +180,39 @@ module Kitchen
199
180
  state_file.read[:last_action]
200
181
  end
201
182
 
183
+ # Extra instance methods used for accessing Puppet data such as a combined
184
+ # run list, node attributes, etc.
185
+ module Cheflike
186
+
187
+ # Returns a combined run_list starting with the platform's run_list
188
+ # followed by the suite's run_list.
189
+ #
190
+ # @return [Array] combined run_list from suite and platform
191
+ def run_list
192
+ Array(platform.run_list) + Array(suite.run_list)
193
+ end
194
+
195
+ # Returns a merged hash of Chef node attributes with values from the
196
+ # suite overriding values from the platform.
197
+ #
198
+ # @return [Hash] merged hash of Chef node attributes
199
+ def attributes
200
+ platform.attributes.rmerge(suite.attributes)
201
+ end
202
+
203
+ def dna
204
+ attributes.rmerge({ :run_list => run_list })
205
+ end
206
+ end
207
+
208
+ # Extra instances methods used for accessing Puppet data such as a run,
209
+ # node attributes, etc.
210
+ module Puppetlike
211
+
212
+ def manifest
213
+ end
214
+ end
215
+
202
216
  private
203
217
 
204
218
  def validate_options(opts)
@@ -0,0 +1,34 @@
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
+ module Kitchen
20
+
21
+ # Value object to track a shell command that will be passed to Kernel.exec
22
+ # for execution.
23
+ #
24
+ # @author Fletcher Nichol <fnichol@nichol.ca>
25
+ class LoginCommand
26
+
27
+ attr_reader :cmd_array, :options
28
+
29
+ def initialize(cmd_array, options = {})
30
+ @cmd_array = cmd_array
31
+ @options = options
32
+ end
33
+ end
34
+ end
@@ -28,30 +28,40 @@ module Kitchen
28
28
  # @return [String] logical name of this platform
29
29
  attr_reader :name
30
30
 
31
- # @return [Array] Array of Chef run_list items
32
- attr_reader :run_list
33
-
34
- # @return [Hash] Hash of Chef node attributes
35
- attr_reader :attributes
36
-
37
31
  # Constructs a new platform.
38
32
  #
39
33
  # @param [Hash] options configuration for a new platform
40
34
  # @option options [String] :name logical name of this platform
41
35
  # (**Required**)
42
- # @option options [Array<String>] :run_list Array of Chef run_list
43
- # items
44
- # @option options [Hash] :attributes Hash of Chef node attributes
45
36
  def initialize(options = {})
37
+ options = options.dup
46
38
  validate_options(options)
47
39
 
48
- @name = options[:name]
49
- @run_list = Array(options[:run_list])
50
- @attributes = options[:attributes] || Hash.new
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
51
59
  end
52
60
 
53
61
  private
54
62
 
63
+ attr_reader :data
64
+
55
65
  def validate_options(opts)
56
66
  [:name].each do |k|
57
67
  raise ClientError, "Platform#new requires option :#{k}" if opts[k].nil?
@@ -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 'thor/util'
20
+
21
+ module Kitchen
22
+
23
+ # A provisioner is responsible for generating the commands necessary to
24
+ # install set up and use a configuration management tool such as Chef and
25
+ # Puppet.
26
+ #
27
+ # @author Fletcher Nichol <fnichol@nichol.ca>
28
+ module Provisioner
29
+
30
+ # Returns an instance of a provisioner given a plugin type string.
31
+ #
32
+ # @param plugin [String] a provisioner plugin type, to be constantized
33
+ # @return [Provisioner::Base] a driver instance
34
+ # @raise [ClientError] if a provisioner instance could not be created
35
+ def self.for_plugin(plugin, instance, config)
36
+ require("kitchen/provisioner/#{plugin}")
37
+
38
+ str_const = Thor::Util.camel_case(plugin)
39
+ klass = self.const_get(str_const)
40
+ klass.new(instance, config)
41
+ rescue UserError
42
+ raise
43
+ rescue LoadError, NameError
44
+ raise ClientError,
45
+ "Could not load the '#{plugin}' provisioner from the load path." +
46
+ " Please ensure that your provisioner is installed as a gem or" +
47
+ " included in your Gemfile if using Bundler."
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,63 @@
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
+ module Kitchen
20
+
21
+ module Provisioner
22
+
23
+ # Base class for a provisioner.
24
+ #
25
+ # @author Fletcher Nichol <fnichol@nichol.ca>
26
+ class Base
27
+
28
+ include Logging
29
+
30
+ def initialize(instance, config)
31
+ @instance = instance
32
+ @config = config
33
+ @logger = instance.logger
34
+ end
35
+
36
+ def install_command ; end
37
+
38
+ def init_command ; end
39
+
40
+ def create_sandbox ; end
41
+
42
+ def prepare_command ; end
43
+
44
+ def run_command ; end
45
+
46
+ def cleanup_sandbox ; end
47
+
48
+ def home_path ; end
49
+
50
+ protected
51
+
52
+ attr_reader :instance, :logger, :config, :tmpdir
53
+
54
+ def sudo(script)
55
+ config[:sudo] ? "sudo -E #{script}" : script
56
+ end
57
+
58
+ def kitchen_root
59
+ config[:kitchen_root]
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,290 @@
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 'fileutils'
20
+ require 'json'
21
+
22
+ module Kitchen
23
+
24
+ module Provisioner
25
+
26
+ # Common implementation details for Chef-related provisioners.
27
+ #
28
+ # @author Fletcher Nichol <fnichol@nichol.ca>
29
+ class ChefBase < Base
30
+
31
+ def install_command
32
+ return nil unless config[:require_chef_omnibus]
33
+
34
+ url = "https://www.opscode.com/chef/install.sh"
35
+ flag = config[:require_chef_omnibus]
36
+ version = if flag.is_a?(String) && flag != "latest"
37
+ "-s -- -v #{flag.downcase}"
38
+ else
39
+ ""
40
+ end
41
+
42
+ <<-INSTALL.gsub(/^ {10}/, '')
43
+ bash -c '
44
+ should_update_chef() {
45
+ case "#{flag}" in
46
+ true|$(chef-solo -v | cut -d " " -f 2)) return 1 ;;
47
+ latest|*) return 0 ;;
48
+ esac
49
+ }
50
+
51
+ if [ ! -d "/opt/chef" ] || should_update_chef ; then
52
+ PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
53
+ export PATH
54
+ echo "-----> Installing Chef Omnibus (#{flag})"
55
+ if command -v wget >/dev/null ; then
56
+ wget #{url} -O - | #{sudo('bash')} #{version}
57
+ elif command -v curl >/dev/null ; then
58
+ curl -sSL #{url} | #{sudo('bash')} #{version}
59
+ else
60
+ echo ">>>>>> Neither wget nor curl found on this instance."
61
+ exit 16
62
+ fi
63
+ fi'
64
+ INSTALL
65
+ end
66
+
67
+ def init_command
68
+ "#{sudo('rm')} -rf #{home_path}"
69
+ end
70
+
71
+ def cleanup_sandbox
72
+ return if tmpdir.nil?
73
+
74
+ debug("Cleaning up local sandbox in #{tmpdir}")
75
+ FileUtils.rmtree(tmpdir)
76
+ end
77
+
78
+ protected
79
+
80
+ def create_chef_sandbox
81
+ @tmpdir = Dir.mktmpdir("#{instance.name}-sandbox-")
82
+ debug("Creating local sandbox in #{tmpdir}")
83
+
84
+ yield if block_given?
85
+ prepare_json
86
+ prepare_data_bags
87
+ prepare_roles
88
+ prepare_nodes
89
+ prepare_secret
90
+ prepare_cache
91
+ prepare_cookbooks
92
+ tmpdir
93
+ end
94
+
95
+ def prepare_json
96
+ File.open(File.join(tmpdir, "dna.json"), "wb") do |file|
97
+ file.write(instance.dna.to_json)
98
+ end
99
+ end
100
+
101
+ def prepare_data_bags
102
+ return unless data_bags
103
+
104
+ info("Preparing data bags")
105
+ debug("Using data bags from #{data_bags}")
106
+
107
+ tmpbags_dir = File.join(tmpdir, "data_bags")
108
+ FileUtils.mkdir_p(tmpbags_dir)
109
+ FileUtils.cp_r(Dir.glob("#{data_bags}/*"), tmpbags_dir)
110
+ end
111
+
112
+ def prepare_roles
113
+ return unless roles
114
+
115
+ info("Preparing roles")
116
+ debug("Using roles from #{roles}")
117
+
118
+ tmproles_dir = File.join(tmpdir, "roles")
119
+ FileUtils.mkdir_p(tmproles_dir)
120
+ FileUtils.cp_r(Dir.glob("#{roles}/*"), tmproles_dir)
121
+ end
122
+
123
+ def prepare_nodes
124
+ return unless nodes
125
+
126
+ info("Preparing nodes")
127
+ debug("Using nodes from #{nodes}")
128
+
129
+ tmpnodes_dir = File.join(tmpdir, "nodes")
130
+ FileUtils.mkdir_p(tmpnodes_dir)
131
+ FileUtils.cp_r(Dir.glob("#{nodes}/*"), tmpnodes_dir)
132
+ end
133
+
134
+ def prepare_secret
135
+ return unless secret
136
+
137
+ info("Preparing encrypted data bag secret")
138
+ debug("Using secret from #{secret}")
139
+
140
+ FileUtils.cp_r(secret, File.join(tmpdir, "encrypted_data_bag_secret"))
141
+ end
142
+
143
+ def prepare_cache
144
+ FileUtils.mkdir_p(File.join(tmpdir, "cache"))
145
+ end
146
+
147
+ def prepare_cookbooks
148
+ if File.exists?(berksfile)
149
+ resolve_with_berkshelf
150
+ elsif File.exists?(cheffile)
151
+ resolve_with_librarian
152
+ elsif File.directory?(cookbooks_dir)
153
+ cp_cookbooks
154
+ elsif File.exists?(metadata_rb)
155
+ cp_this_cookbook
156
+ else
157
+ FileUtils.rmtree(tmpdir)
158
+ fatal("Berksfile, Cheffile, cookbooks/, or metadata.rb" +
159
+ " must exist in #{kitchen_root}")
160
+ raise UserError, "Cookbooks could not be found"
161
+ end
162
+
163
+ filter_only_cookbook_files
164
+ end
165
+
166
+ def filter_only_cookbook_files
167
+ info("Removing non-cookbook files in sandbox")
168
+
169
+ all_files = Dir.glob(File.join(tmpbooks_dir, "**/*")).
170
+ select { |fn| File.file?(fn) }
171
+ cookbook_files = Dir.glob(File.join(tmpbooks_dir, cookbook_files_glob)).
172
+ select { |fn| File.file?(fn) }
173
+
174
+ FileUtils.rm(all_files - cookbook_files)
175
+ end
176
+
177
+ def cookbook_files_glob
178
+ files = %w{README.* metadata.{json,rb}
179
+ attributes/**/* definitions/**/* files/**/* libraries/**/*
180
+ providers/**/* recipes/**/* resources/**/* templates/**/*
181
+ }
182
+
183
+ "*/{#{files.join(',')}}"
184
+ end
185
+
186
+ def berksfile
187
+ File.join(kitchen_root, "Berksfile")
188
+ end
189
+
190
+ def cheffile
191
+ File.join(kitchen_root, "Cheffile")
192
+ end
193
+
194
+ def metadata_rb
195
+ File.join(kitchen_root, "metadata.rb")
196
+ end
197
+
198
+ def cookbooks_dir
199
+ File.join(kitchen_root, "cookbooks")
200
+ end
201
+
202
+ def data_bags
203
+ instance.suite.data_bags_path
204
+ end
205
+
206
+ def roles
207
+ instance.suite.roles_path
208
+ end
209
+
210
+ def nodes
211
+ instance.suite.nodes_path
212
+ end
213
+
214
+ def secret
215
+ instance.suite.encrypted_data_bag_secret_key_path
216
+ end
217
+
218
+ def tmpbooks_dir
219
+ File.join(tmpdir, "cookbooks")
220
+ end
221
+
222
+ def cp_cookbooks
223
+ info("Preparing cookbooks from project directory")
224
+ debug("Using cookbooks from #{cookbooks_dir}")
225
+
226
+ FileUtils.mkdir_p(tmpbooks_dir)
227
+ FileUtils.cp_r(File.join(cookbooks_dir, "."), tmpbooks_dir)
228
+ cp_this_cookbook if File.exists?(metadata_rb)
229
+ end
230
+
231
+ def cp_this_cookbook
232
+ info("Preparing current project directory as a cookbook")
233
+ debug("Using metadata.rb from #{metadata_rb}")
234
+
235
+ cb_name = MetadataChopper.extract(metadata_rb).first or raise(UserError,
236
+ "The metadata.rb does not define the 'name' key." +
237
+ " Please add: `name '<cookbook_name>'` to metadata.rb and retry")
238
+
239
+ cb_path = File.join(tmpbooks_dir, cb_name)
240
+ glob = Dir.glob("#{kitchen_root}/{metadata.rb,README.*," +
241
+ "attributes,files,libraries,providers,recipes,resources,templates}")
242
+
243
+ FileUtils.mkdir_p(cb_path)
244
+ FileUtils.cp_r(glob, cb_path)
245
+ end
246
+
247
+ def resolve_with_berkshelf
248
+ info("Resolving cookbook dependencies with Berkshelf")
249
+ debug("Using Berksfile from #{berksfile}")
250
+
251
+ begin
252
+ require 'berkshelf'
253
+ rescue LoadError
254
+ fatal("The `berkself' gem is missing and must be installed." +
255
+ " Run `gem install berkshelf` or add the following " +
256
+ "to your Gemfile if you are using Bundler: `gem 'berkshelf'`.")
257
+ raise UserError, "Could not load Berkshelf"
258
+ end
259
+
260
+ Kitchen.mutex.synchronize do
261
+ Berkshelf::Berksfile.from_file(berksfile).
262
+ install(:path => tmpbooks_dir)
263
+ end
264
+ end
265
+
266
+ def resolve_with_librarian
267
+ info("Resolving cookbook dependencies with Librarian-Chef")
268
+ debug("Using Cheffile from #{cheffile}")
269
+
270
+ begin
271
+ require 'librarian/chef/environment'
272
+ require 'librarian/action/resolve'
273
+ require 'librarian/action/install'
274
+ rescue LoadError
275
+ fatal("The `librarian-chef' gem is missing and must be installed." +
276
+ " Run `gem install librarian-chef` or add the following " +
277
+ "to your Gemfile if you are using Bundler: `gem 'librarian-chef'`.")
278
+ raise UserError, "Could not load Librarian-Chef"
279
+ end
280
+
281
+ Kitchen.mutex.synchronize do
282
+ env = Librarian::Chef::Environment.new
283
+ env.config_db.local["path"] = tmpbooks_dir
284
+ Librarian::Action::Resolve.new(env).run
285
+ Librarian::Action::Install.new(env).run
286
+ end
287
+ end
288
+ end
289
+ end
290
+ end