test-kitchen 1.0.0.alpha.0 → 1.0.0.alpha.1

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