test-kitchen 1.0.0.alpha.0 → 1.0.0.alpha.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.
@@ -26,11 +26,13 @@ module Kitchen
26
26
  # @return [Driver::Base] a driver instance
27
27
  # @raise [ClientError] if a driver instance could not be created
28
28
  def self.for_plugin(plugin, config)
29
- require "kitchen/driver/#{plugin}"
29
+ first_load = require("kitchen/driver/#{plugin}")
30
30
 
31
31
  str_const = Util.to_camel_case(plugin)
32
32
  klass = self.const_get(str_const)
33
- klass.new(config)
33
+ object = klass.new(config)
34
+ object.verify_dependencies if first_load
35
+ object
34
36
  rescue UserError
35
37
  raise
36
38
  rescue LoadError
@@ -20,6 +20,20 @@ module Kitchen
20
20
 
21
21
  module Driver
22
22
 
23
+ # Value object to track a shell command that will be passed to Kernel.exec
24
+ # for execution.
25
+ #
26
+ # @author Fletcher Nichol <fnichol@nichol.ca>
27
+ class LoginCommand
28
+
29
+ attr_reader :cmd_array, :options
30
+
31
+ def initialize(cmd_array, options = {})
32
+ @cmd_array = cmd_array
33
+ @options = options
34
+ end
35
+ end
36
+
23
37
  # Base class for a driver. A driver is responsible for carrying out the
24
38
  # lifecycle activities of an instance, such as creating, converging, and
25
39
  # destroying an instance.
@@ -84,16 +98,25 @@ module Kitchen
84
98
  # @raise [ActionFailed] if the action could not be completed
85
99
  def destroy(state) ; end
86
100
 
87
- # Returns the shell command array that will log into an instance.
101
+ # Returns the shell command that will log into an instance.
88
102
  #
89
103
  # @param state [Hash] mutable instance and driver state
90
- # @return [Array] an array of command line tokens to be used in a
91
- # fork/exec
104
+ # @return [LoginCommand] an object containing the array of command line
105
+ # tokens and exec options to be used in a fork/exec
92
106
  # @raise [ActionFailed] if the action could not be completed
93
107
  def login_command(state)
94
108
  raise ActionFailed, "Remote login is not supported in this driver."
95
109
  end
96
110
 
111
+ # Performs whatever tests that may be required to ensure that this driver
112
+ # will be able to function in the current environment. This may involve
113
+ # checking for the presence of certain directories, software installed,
114
+ # etc.
115
+ #
116
+ # @raise [UserError] if the driver will not be able to perform or if a
117
+ # documented dependency is missing from the system
118
+ def verify_dependencies ; end
119
+
97
120
  protected
98
121
 
99
122
  attr_reader :config, :instance
@@ -102,7 +125,7 @@ module Kitchen
102
125
  map(&:to_sym).freeze
103
126
 
104
127
  def logger
105
- instance.logger
128
+ instance ? instance.logger : Kitchen.logger
106
129
  end
107
130
 
108
131
  def puts(msg)
@@ -113,11 +136,12 @@ module Kitchen
113
136
  info(msg)
114
137
  end
115
138
 
116
- def run_command(cmd, use_sudo = nil, log_subject = nil)
117
- use_sudo = config[:use_sudo] if use_sudo.nil?
118
- log_subject = Util.to_snake_case(self.class.to_s)
119
-
120
- super(cmd, use_sudo, log_subject)
139
+ def run_command(cmd, options = {})
140
+ base_options = {
141
+ :use_sudo => config[:use_sudo],
142
+ :log_subject => Util.to_snake_case(self.class.to_s)
143
+ }.merge(options)
144
+ super(cmd, base_options)
121
145
  end
122
146
 
123
147
  def kb_setup_cmd
@@ -71,7 +71,7 @@ module Kitchen
71
71
  args += %W{ -i #{config[:ssh_key]}} if config[:ssh_key]
72
72
  args += %W{ #{config[:username]}@#{state[:hostname]}}
73
73
 
74
- ["ssh", *args]
74
+ Driver::LoginCommand.new(["ssh", *args])
75
75
  end
76
76
 
77
77
  protected
@@ -91,9 +91,9 @@ module Kitchen
91
91
  end
92
92
 
93
93
  def install_omnibus(ssh_args)
94
- flag = config[:require_chef_omnibus].downcase
94
+ flag = config[:require_chef_omnibus]
95
95
  version = if flag.is_a?(String) && flag != "latest"
96
- "-s -- -v #{flag}"
96
+ "-s -- -v #{flag.downcase}"
97
97
  else
98
98
  ""
99
99
  end
@@ -0,0 +1,196 @@
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/group'
20
+
21
+ module Kitchen
22
+
23
+ module Generator
24
+
25
+ # A project initialization generator, to help prepare a cookbook project
26
+ # for testing with Kitchen.
27
+ #
28
+ # @author Fletcher Nichol <fnichol@nichol.ca>
29
+ class Init < Thor::Group
30
+
31
+ include Thor::Actions
32
+
33
+ class_option :driver, :type => :array, :aliases => "-D",
34
+ :default => "kitchen-vagrant",
35
+ :desc => <<-D.gsub(/^\s+/, '').gsub(/\n/, ' ')
36
+ One or more Kitchen Driver gems to be installed or added to a
37
+ Gemfile
38
+ D
39
+
40
+ class_option :create_gemfile, :type => :boolean, :default => false,
41
+ :desc => <<-D.gsub(/^\s+/, '').gsub(/\n/, ' ')
42
+ Whether or not to create a Gemfile if one does not exist.
43
+ Default: false
44
+ D
45
+
46
+ def init
47
+ create_file ".kitchen.yml", default_yaml
48
+
49
+ rakedoc = <<-RAKE.gsub(/^ {10}/, '')
50
+
51
+ begin
52
+ require 'kitchen/rake_tasks'
53
+ Kitchen::RakeTasks.new
54
+ rescue LoadError
55
+ puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV['CI']
56
+ end
57
+ RAKE
58
+ append_to_file("Rakefile", rakedoc) if init_rakefile?
59
+
60
+ thordoc = <<-THOR.gsub(/^ {10}/, '')
61
+
62
+ begin
63
+ require 'kitchen/thor_tasks'
64
+ Kitchen::ThorTasks.new
65
+ rescue LoadError
66
+ puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV['CI']
67
+ end
68
+ THOR
69
+ append_to_file("Thorfile", thordoc) if init_thorfile?
70
+
71
+ empty_directory "test/integration/default" if init_test_dir?
72
+ append_to_gitignore(".kitchen/")
73
+ append_to_gitignore(".kitchen.local.yml")
74
+ prepare_gemfile if File.exists?("Gemfile") || options[:create_gemfile]
75
+ add_drivers
76
+
77
+ if @display_bundle_msg
78
+ say "You must run `bundle install' to fetch any new gems.", :red
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def default_yaml
85
+ cookbook_name = if File.exists?(File.expand_path('metadata.rb'))
86
+ MetadataChopper.extract('metadata.rb').first
87
+ else
88
+ nil
89
+ end
90
+ run_list = cookbook_name ? "recipe[#{cookbook_name}]" : nil
91
+ driver_plugin = Array(options[:driver]).first || 'dummy'
92
+
93
+ { 'driver_plugin' => driver_plugin.sub(/^kitchen-/, ''),
94
+ 'platforms' => platforms_hash,
95
+ 'suites' => [
96
+ { 'name' => 'default',
97
+ 'run_list' => Array(run_list),
98
+ 'attributes' => Hash.new
99
+ },
100
+ ]
101
+ }.to_yaml
102
+ end
103
+
104
+ def platforms_hash
105
+ url_base = "https://opscode-vm.s3.amazonaws.com/vagrant/boxes"
106
+ platforms = [
107
+ { :n => 'ubuntu', :vers => %w(12.04 10.04), :rl => "recipe[apt]" },
108
+ { :n => 'centos', :vers => %w(6.3 5.8), :rl => "recipe[yum::epel]" },
109
+ ]
110
+ platforms = platforms.map do |p|
111
+ p[:vers].map do |v|
112
+ { 'name' => "#{p[:n]}-#{v}",
113
+ 'driver_config' => {
114
+ 'box' => "opscode-#{p[:n]}-#{v}",
115
+ 'box_url' => "#{url_base}/opscode-#{p[:n]}-#{v}.box"
116
+ },
117
+ 'run_list' => Array(p[:rl])
118
+ }
119
+ end
120
+ end.flatten
121
+ end
122
+
123
+ def init_rakefile?
124
+ File.exists?("Rakefile") &&
125
+ not_in_file?("Rakefile", %r{require 'kitchen/rake_tasks'})
126
+ end
127
+
128
+ def init_thorfile?
129
+ File.exists?("Thorfile") &&
130
+ not_in_file?("Thorfile", %r{require 'kitchen/thor_tasks'})
131
+ end
132
+
133
+ def init_test_dir?
134
+ Dir.glob("test/integration/*").select { |d| File.directory?(d) }.empty?
135
+ end
136
+
137
+ def append_to_gitignore(line)
138
+ create_file(".gitignore") unless File.exists?(".gitignore")
139
+
140
+ if IO.readlines(".gitignore").grep(%r{^#{line}}).empty?
141
+ append_to_file(".gitignore", "#{line}\n")
142
+ end
143
+ end
144
+
145
+ def prepare_gemfile
146
+ create_gemfile_if_missing
147
+ add_gem_to_gemfile
148
+ end
149
+
150
+ def create_gemfile_if_missing
151
+ unless File.exists?("Gemfile")
152
+ create_file("Gemfile", %{source 'https://rubygems.org'\n\n})
153
+ end
154
+ end
155
+
156
+ def add_gem_to_gemfile
157
+ if not_in_file?("Gemfile", %r{gem 'test-kitchen'})
158
+ append_to_file("Gemfile",
159
+ %{gem 'test-kitchen', :group => :integration\n})
160
+ @display_bundle_msg = true
161
+ end
162
+ end
163
+
164
+ def add_drivers
165
+ return if options[:driver].nil? || options[:driver].empty?
166
+ display_warning = false
167
+
168
+ Array(options[:driver]).each do |driver_gem|
169
+ if File.exists?("Gemfile") || options[:create_gemfile]
170
+ add_driver_to_gemfile(driver_gem)
171
+ else
172
+ install_gem(driver_gem)
173
+ end
174
+ end
175
+ end
176
+
177
+ def add_driver_to_gemfile(driver_gem)
178
+ if not_in_file?("Gemfile", %r{gem '#{driver_gem}'})
179
+ append_to_file("Gemfile",
180
+ %{gem '#{driver_gem}', :group => :integration\n})
181
+ @display_bundle_msg = true
182
+ end
183
+ end
184
+
185
+ def install_gem(driver_gem)
186
+ Bundler.with_clean_env do
187
+ run "gem install #{driver_gem}"
188
+ end
189
+ end
190
+
191
+ def not_in_file?(filename, regexp)
192
+ IO.readlines(filename).grep(regexp).empty?
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,190 @@
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/group'
20
+
21
+ module Kitchen
22
+
23
+ module Generator
24
+
25
+ # A generator to create a new Kitchen Driver gem project.
26
+ #
27
+ # @author Fletcher Nichol <fnichol@nichol.ca>
28
+ class NewPlugin < Thor::Group
29
+
30
+ include Thor::Actions
31
+
32
+ argument :plugin_name
33
+
34
+ class_option :license, :aliases => "-l", :default => "apachev2",
35
+ :desc => "License type for gem (apachev2, mit, gplv3, gplv2, reserved)"
36
+
37
+ def new_plugin
38
+ if ! run("command -v bundle", :verbose => false)
39
+ die "Bundler must be installed and on your PATH: `gem install bundler'"
40
+ end
41
+
42
+ @plugin_name = plugin_name
43
+ @gem_name = "kitchen-#{plugin_name}"
44
+ @gemspec = "#{gem_name}.gemspec"
45
+ @klass_name = Util.to_camel_case(plugin_name)
46
+ @constant = Util.to_snake_case(plugin_name).upcase
47
+ @license = options[:license]
48
+ @author = %x{git config user.name}.chomp
49
+ @email = %x{git config user.email}.chomp
50
+ @year = Time.now.year
51
+
52
+ create_plugin
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :plugin_name, :gem_name, :gemspec, :klass_name,
58
+ :constant, :license, :author, :email, :year
59
+
60
+ def create_plugin
61
+ run("bundle gem #{gem_name}") unless File.directory?(gem_name)
62
+
63
+ inside(gem_name) do
64
+ update_gemspec
65
+ update_gemfile
66
+ update_rakefile
67
+ create_src_files
68
+ cleanup
69
+ create_license
70
+ add_git_files
71
+ end
72
+ end
73
+
74
+ def update_gemspec
75
+ gsub_file(gemspec, %r{require '#{gem_name}/version'},
76
+ %{require 'kitchen/driver/#{plugin_name}_version.rb'})
77
+ gsub_file(gemspec, %r{Kitchen::#{klass_name}::VERSION},
78
+ %{Kitchen::Driver::#{constant}_VERSION})
79
+ gsub_file(gemspec, %r{(gem\.executables\s*) =.*$},
80
+ '\1 = []')
81
+ gsub_file(gemspec, %r{(gem\.description\s*) =.*$},
82
+ '\1 = "' + "Kitchen::Driver::#{klass_name} - " +
83
+ "A Kitchen Driver for #{klass_name}\"")
84
+ gsub_file(gemspec, %r{(gem\.summary\s*) =.*$},
85
+ '\1 = gem.description')
86
+ gsub_file(gemspec, %r{(gem\.homepage\s*) =.*$},
87
+ '\1 = "https://github.com/opscode/' +
88
+ "#{gem_name}/\"")
89
+ insert_into_file(gemspec,
90
+ "\n gem.add_dependency 'test-kitchen'\n", :before => "end\n")
91
+ insert_into_file(gemspec,
92
+ "\n gem.add_development_dependency 'cane'\n", :before => "end\n")
93
+ insert_into_file(gemspec,
94
+ " gem.add_development_dependency 'tailor'\n", :before => "end\n")
95
+ end
96
+
97
+ def update_gemfile
98
+ append_to_file("Gemfile", "\ngroup :test do\n gem 'rake'\nend\n")
99
+ end
100
+
101
+ def update_rakefile
102
+ append_to_file("Rakefile", <<-RAKEFILE.gsub(/^ {10}/, ''))
103
+ require 'cane/rake_task'
104
+ require 'tailor/rake_task'
105
+
106
+ desc "Run cane to check quality metrics"
107
+ Cane::RakeTask.new
108
+
109
+ Tailor::RakeTask.new
110
+
111
+ task :default => [ :cane, :tailor ]
112
+ RAKEFILE
113
+ end
114
+
115
+ def create_src_files
116
+ license_comments = rendered_license.gsub(/^/, '# ').gsub(/\s+$/, '')
117
+
118
+ empty_directory("lib/kitchen/driver")
119
+ create_template("plugin/version.rb",
120
+ "lib/kitchen/driver/#{plugin_name}_version.rb",
121
+ :klass_name => klass_name, :constant => constant,
122
+ :license => license_comments)
123
+ create_template("plugin/driver.rb",
124
+ "lib/kitchen/driver/#{plugin_name}.rb",
125
+ :klass_name => klass_name, :license => license_comments,
126
+ :author => author, :email => email)
127
+ end
128
+
129
+ def rendered_license
130
+ TemplateRenderer.render("plugin/license_#{license}",
131
+ :author => author, :email => email, :year => year)
132
+ end
133
+
134
+ def create_license
135
+ dest_file = case license
136
+ when "mit" then "LICENSE.txt"
137
+ when "apachev2", "reserved" then "LICENSE"
138
+ when "gplv2", "gplv3" then "COPYING"
139
+ else
140
+ raise ArgumentError, "No such license #{license}"
141
+ end
142
+
143
+ create_file(dest_file, rendered_license)
144
+ end
145
+
146
+ def cleanup
147
+ %W(LICENSE.txt lib/#{gem_name}/version.rb lib/#{gem_name}.rb).each do |f|
148
+ run("git rm -f #{f}") if File.exists?(f)
149
+ end
150
+ remove_dir("lib/#{gem_name}")
151
+ end
152
+
153
+ def add_git_files
154
+ run("git add .")
155
+ end
156
+
157
+ def create_template(template, destination, data = {})
158
+ create_file(destination, TemplateRenderer.render(template, data))
159
+ end
160
+
161
+ # Renders an ERB template with a hash of template variables.
162
+ #
163
+ # @author Fletcher Nichol <fnichol@nichol.ca>
164
+ class TemplateRenderer < OpenStruct
165
+
166
+ def self.render(template, data = {})
167
+ renderer = new(template, data)
168
+ yield renderer if block_given?
169
+ renderer.render
170
+ end
171
+
172
+ def initialize(template, data = {})
173
+ super()
174
+ data[:template] = template
175
+ data.each { |key, value| send("#{key}=", value) }
176
+ end
177
+
178
+ def render
179
+ ERB.new(IO.read(template_file)).result(binding)
180
+ end
181
+
182
+ private
183
+
184
+ def template_file
185
+ Kitchen.source_root.join("templates", "#{template}.erb").to_s
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end