test-kitchen 1.2.1 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.cane +1 -1
- data/.rubocop.yml +3 -0
- data/.travis.yml +20 -9
- data/CHANGELOG.md +219 -108
- data/Gemfile +10 -6
- data/Guardfile +38 -9
- data/README.md +11 -1
- data/Rakefile +21 -37
- data/bin/kitchen +4 -4
- data/features/kitchen_action_commands.feature +161 -0
- data/features/kitchen_console_command.feature +34 -0
- data/features/kitchen_diagnose_command.feature +64 -0
- data/features/kitchen_init_command.feature +29 -17
- data/features/kitchen_list_command.feature +2 -2
- data/features/kitchen_login_command.feature +56 -0
- data/features/{sink_command.feature → kitchen_sink_command.feature} +0 -0
- data/features/kitchen_test_command.feature +88 -0
- data/features/step_definitions/gem_steps.rb +8 -6
- data/features/step_definitions/git_steps.rb +4 -2
- data/features/step_definitions/output_steps.rb +5 -0
- data/features/support/env.rb +12 -9
- data/lib/kitchen.rb +60 -38
- data/lib/kitchen/base64_stream.rb +55 -0
- data/lib/kitchen/busser.rb +124 -58
- data/lib/kitchen/cli.rb +121 -38
- data/lib/kitchen/collection.rb +3 -3
- data/lib/kitchen/color.rb +4 -4
- data/lib/kitchen/command.rb +78 -11
- data/lib/kitchen/command/action.rb +3 -2
- data/lib/kitchen/command/console.rb +12 -5
- data/lib/kitchen/command/diagnose.rb +17 -3
- data/lib/kitchen/command/driver_discover.rb +26 -7
- data/lib/kitchen/command/exec.rb +41 -0
- data/lib/kitchen/command/list.rb +44 -14
- data/lib/kitchen/command/login.rb +2 -1
- data/lib/kitchen/command/sink.rb +2 -1
- data/lib/kitchen/command/test.rb +5 -4
- data/lib/kitchen/config.rb +146 -14
- data/lib/kitchen/configurable.rb +314 -0
- data/lib/kitchen/data_munger.rb +522 -18
- data/lib/kitchen/diagnostic.rb +43 -4
- data/lib/kitchen/driver.rb +4 -4
- data/lib/kitchen/driver/base.rb +80 -115
- data/lib/kitchen/driver/dummy.rb +34 -6
- data/lib/kitchen/driver/proxy.rb +14 -3
- data/lib/kitchen/driver/ssh_base.rb +61 -7
- data/lib/kitchen/errors.rb +109 -9
- data/lib/kitchen/generator/driver_create.rb +39 -5
- data/lib/kitchen/generator/init.rb +130 -45
- data/lib/kitchen/instance.rb +162 -28
- data/lib/kitchen/lazy_hash.rb +79 -7
- data/lib/kitchen/loader/yaml.rb +159 -27
- data/lib/kitchen/logger.rb +267 -21
- data/lib/kitchen/logging.rb +30 -3
- data/lib/kitchen/login_command.rb +11 -2
- data/lib/kitchen/metadata_chopper.rb +2 -2
- data/lib/kitchen/provisioner.rb +4 -4
- data/lib/kitchen/provisioner/base.rb +107 -103
- data/lib/kitchen/provisioner/chef/berkshelf.rb +36 -8
- data/lib/kitchen/provisioner/chef/librarian.rb +40 -11
- data/lib/kitchen/provisioner/chef_base.rb +206 -167
- data/lib/kitchen/provisioner/chef_solo.rb +25 -7
- data/lib/kitchen/provisioner/chef_zero.rb +105 -29
- data/lib/kitchen/provisioner/dummy.rb +1 -1
- data/lib/kitchen/provisioner/shell.rb +21 -6
- data/lib/kitchen/rake_tasks.rb +8 -3
- data/lib/kitchen/shell_out.rb +15 -18
- data/lib/kitchen/ssh.rb +122 -27
- data/lib/kitchen/state_file.rb +24 -7
- data/lib/kitchen/thor_tasks.rb +9 -4
- data/lib/kitchen/util.rb +43 -118
- data/lib/kitchen/version.rb +1 -1
- data/lib/vendor/hash_recursive_merge.rb +10 -2
- data/spec/kitchen/base64_stream_spec.rb +77 -0
- data/spec/kitchen/busser_spec.rb +490 -0
- data/spec/kitchen/collection_spec.rb +10 -10
- data/spec/kitchen/color_spec.rb +2 -2
- data/spec/kitchen/config_spec.rb +234 -62
- data/spec/kitchen/configurable_spec.rb +490 -0
- data/spec/kitchen/data_munger_spec.rb +1070 -862
- data/spec/kitchen/diagnostic_spec.rb +79 -0
- data/spec/kitchen/driver/base_spec.rb +80 -85
- data/spec/kitchen/driver/dummy_spec.rb +43 -14
- data/spec/kitchen/driver/proxy_spec.rb +134 -0
- data/spec/kitchen/driver/ssh_base_spec.rb +644 -0
- data/spec/kitchen/driver_spec.rb +15 -15
- data/spec/kitchen/errors_spec.rb +309 -0
- data/spec/kitchen/instance_spec.rb +143 -46
- data/spec/kitchen/lazy_hash_spec.rb +36 -9
- data/spec/kitchen/loader/yaml_spec.rb +237 -226
- data/spec/kitchen/logger_spec.rb +419 -0
- data/spec/kitchen/logging_spec.rb +59 -0
- data/spec/kitchen/login_command_spec.rb +49 -0
- data/spec/kitchen/metadata_chopper_spec.rb +82 -0
- data/spec/kitchen/platform_spec.rb +4 -4
- data/spec/kitchen/provisioner/base_spec.rb +65 -125
- data/spec/kitchen/provisioner/chef_base_spec.rb +798 -0
- data/spec/kitchen/provisioner/chef_solo_spec.rb +316 -0
- data/spec/kitchen/provisioner/chef_zero_spec.rb +624 -0
- data/spec/kitchen/provisioner/shell_spec.rb +269 -0
- data/spec/kitchen/provisioner_spec.rb +6 -6
- data/spec/kitchen/shell_out_spec.rb +143 -0
- data/spec/kitchen/ssh_spec.rb +683 -0
- data/spec/kitchen/state_file_spec.rb +28 -21
- data/spec/kitchen/suite_spec.rb +7 -7
- data/spec/kitchen/util_spec.rb +68 -10
- data/spec/kitchen_spec.rb +107 -0
- data/spec/spec_helper.rb +18 -13
- data/support/chef-client-zero.rb +10 -9
- data/support/chef_helpers.sh +16 -0
- data/support/download_helpers.sh +109 -0
- data/test-kitchen.gemspec +42 -33
- metadata +107 -33
@@ -16,7 +16,8 @@
|
|
16
16
|
# See the License for the specific language governing permissions and
|
17
17
|
# limitations under the License.
|
18
18
|
|
19
|
-
require
|
19
|
+
require "rubygems/gem_runner"
|
20
|
+
require "thor/group"
|
20
21
|
|
21
22
|
module Kitchen
|
22
23
|
|
@@ -30,143 +31,202 @@ module Kitchen
|
|
30
31
|
|
31
32
|
include Thor::Actions
|
32
33
|
|
33
|
-
class_option :driver,
|
34
|
+
class_option :driver,
|
35
|
+
:type => :array,
|
36
|
+
:aliases => "-D",
|
34
37
|
:default => "kitchen-vagrant",
|
35
|
-
:desc => <<-D.gsub(/^\s+/,
|
38
|
+
:desc => <<-D.gsub(/^\s+/, "").gsub(/\n/, " ")
|
36
39
|
One or more Kitchen Driver gems to be installed or added to a
|
37
40
|
Gemfile
|
38
41
|
D
|
39
42
|
|
40
|
-
class_option :provisioner,
|
43
|
+
class_option :provisioner,
|
44
|
+
:type => :string,
|
45
|
+
:aliases => "-P",
|
41
46
|
:default => "chef_solo",
|
42
|
-
:desc => <<-D.gsub(/^\s+/,
|
47
|
+
:desc => <<-D.gsub(/^\s+/, "").gsub(/\n/, " ")
|
43
48
|
The default Kitchen Provisioner to use
|
44
49
|
D
|
45
50
|
|
46
|
-
class_option :create_gemfile,
|
47
|
-
:
|
51
|
+
class_option :create_gemfile,
|
52
|
+
:type => :boolean,
|
53
|
+
:default => false,
|
54
|
+
:desc => <<-D.gsub(/^\s+/, "").gsub(/\n/, " ")
|
48
55
|
Whether or not to create a Gemfile if one does not exist.
|
49
56
|
Default: false
|
50
57
|
D
|
51
58
|
|
59
|
+
# Invoke the command.
|
52
60
|
def init
|
53
61
|
self.class.source_root(Kitchen.source_root.join("templates", "init"))
|
54
62
|
|
55
63
|
create_kitchen_yaml
|
56
|
-
prepare_rakefile
|
57
|
-
prepare_thorfile
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
append_to_gitignore(".kitchen.local.yml")
|
62
|
-
end
|
63
|
-
prepare_gemfile if init_gemfile?
|
64
|
+
prepare_rakefile
|
65
|
+
prepare_thorfile
|
66
|
+
create_test_dir
|
67
|
+
prepare_gitignore
|
68
|
+
prepare_gemfile
|
64
69
|
add_drivers
|
65
|
-
|
66
|
-
if @display_bundle_msg
|
67
|
-
say "You must run `bundle install' to fetch any new gems.", :red
|
68
|
-
end
|
70
|
+
display_bundle_message
|
69
71
|
end
|
70
72
|
|
71
73
|
private
|
72
74
|
|
75
|
+
# Creates the `.kitchen.yml` file.
|
76
|
+
#
|
77
|
+
# @api private
|
73
78
|
def create_kitchen_yaml
|
74
|
-
cookbook_name = if File.
|
75
|
-
MetadataChopper.extract(
|
79
|
+
cookbook_name = if File.exist?(File.expand_path("metadata.rb"))
|
80
|
+
MetadataChopper.extract("metadata.rb").first
|
76
81
|
else
|
77
82
|
nil
|
78
83
|
end
|
79
84
|
run_list = cookbook_name ? "recipe[#{cookbook_name}::default]" : nil
|
80
|
-
driver_plugin = Array(options[:driver]).first ||
|
85
|
+
driver_plugin = Array(options[:driver]).first || "dummy"
|
81
86
|
|
82
|
-
template("kitchen.yml.erb", ".kitchen.yml",
|
83
|
-
:driver_plugin => driver_plugin.sub(/^kitchen-/,
|
87
|
+
template("kitchen.yml.erb", ".kitchen.yml",
|
88
|
+
:driver_plugin => driver_plugin.sub(/^kitchen-/, ""),
|
84
89
|
:provisioner => options[:provisioner],
|
85
90
|
:run_list => Array(run_list)
|
86
|
-
|
91
|
+
)
|
87
92
|
end
|
88
93
|
|
94
|
+
# @return [true,false] whether or not a Gemfile needs to be initialized
|
95
|
+
# @api private
|
89
96
|
def init_gemfile?
|
90
|
-
File.
|
97
|
+
File.exist?(File.join(destination_root, "Gemfile")) ||
|
91
98
|
options[:create_gemfile]
|
92
99
|
end
|
93
100
|
|
101
|
+
# @return [true,false] whether or not a Rakefile needs to be initialized
|
102
|
+
# @api private
|
94
103
|
def init_rakefile?
|
95
|
-
File.
|
104
|
+
File.exist?(File.join(destination_root, "Rakefile")) &&
|
96
105
|
not_in_file?("Rakefile", %r{require 'kitchen/rake_tasks'})
|
97
106
|
end
|
98
107
|
|
108
|
+
# @return [true,false] whether or not a Thorfile needs to be initialized
|
109
|
+
# @api private
|
99
110
|
def init_thorfile?
|
100
|
-
File.
|
111
|
+
File.exist?(File.join(destination_root, "Thorfile")) &&
|
101
112
|
not_in_file?("Thorfile", %r{require 'kitchen/thor_tasks'})
|
102
113
|
end
|
103
114
|
|
115
|
+
# @return [true,false] whether or not a test directory needs to be
|
116
|
+
# initialized
|
117
|
+
# @api private
|
104
118
|
def init_test_dir?
|
105
119
|
Dir.glob("test/integration/*").select { |d| File.directory?(d) }.empty?
|
106
120
|
end
|
107
121
|
|
122
|
+
# @return [true,false] whether or not a `.gitignore` file needs to be
|
123
|
+
# initialized
|
124
|
+
# @api private
|
108
125
|
def init_git?
|
109
|
-
File.directory?(File.join(destination_root,
|
126
|
+
File.directory?(File.join(destination_root, ".git"))
|
110
127
|
end
|
111
128
|
|
129
|
+
# Prepares a Rakefile.
|
130
|
+
#
|
131
|
+
# @api private
|
112
132
|
def prepare_rakefile
|
113
|
-
|
133
|
+
return unless init_rakefile?
|
134
|
+
|
135
|
+
rakedoc = <<-RAKE.gsub(/^ {10}/, "")
|
114
136
|
|
115
137
|
begin
|
116
|
-
require
|
138
|
+
require "kitchen/rake_tasks"
|
117
139
|
Kitchen::RakeTasks.new
|
118
140
|
rescue LoadError
|
119
|
-
puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV[
|
141
|
+
puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV["CI"]
|
120
142
|
end
|
121
143
|
RAKE
|
122
144
|
append_to_file(File.join(destination_root, "Rakefile"), rakedoc)
|
123
145
|
end
|
124
146
|
|
147
|
+
# Prepares a Thorfile.
|
148
|
+
#
|
149
|
+
# @api private
|
125
150
|
def prepare_thorfile
|
126
|
-
|
151
|
+
return unless init_thorfile?
|
152
|
+
|
153
|
+
thordoc = <<-THOR.gsub(/^ {10}/, "")
|
127
154
|
|
128
155
|
begin
|
129
|
-
require
|
156
|
+
require "kitchen/thor_tasks"
|
130
157
|
Kitchen::ThorTasks.new
|
131
158
|
rescue LoadError
|
132
|
-
puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV[
|
159
|
+
puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV["CI"]
|
133
160
|
end
|
134
161
|
THOR
|
135
162
|
append_to_file(File.join(destination_root, "Thorfile"), thordoc)
|
136
163
|
end
|
137
164
|
|
165
|
+
# Create the default test directory
|
166
|
+
#
|
167
|
+
# @api private
|
168
|
+
def create_test_dir
|
169
|
+
empty_directory "test/integration/default" if init_test_dir?
|
170
|
+
end
|
171
|
+
|
172
|
+
# Prepares the .gitignore file
|
173
|
+
#
|
174
|
+
# @api private
|
175
|
+
def prepare_gitignore
|
176
|
+
return unless init_git?
|
177
|
+
|
178
|
+
append_to_gitignore(".kitchen/")
|
179
|
+
append_to_gitignore(".kitchen.local.yml")
|
180
|
+
end
|
181
|
+
|
182
|
+
# Appends a line to the .gitignore file.
|
183
|
+
#
|
184
|
+
# @api private
|
138
185
|
def append_to_gitignore(line)
|
139
|
-
create_file(".gitignore") unless File.
|
186
|
+
create_file(".gitignore") unless File.exist?(File.join(destination_root, ".gitignore"))
|
140
187
|
|
141
188
|
if IO.readlines(File.join(destination_root, ".gitignore")).grep(%r{^#{line}}).empty?
|
142
189
|
append_to_file(".gitignore", "#{line}\n")
|
143
190
|
end
|
144
191
|
end
|
145
192
|
|
193
|
+
# Prepares a Gemfile.
|
194
|
+
#
|
195
|
+
# @api private
|
146
196
|
def prepare_gemfile
|
197
|
+
return unless init_gemfile?
|
198
|
+
|
147
199
|
create_gemfile_if_missing
|
148
200
|
add_gem_to_gemfile
|
149
201
|
end
|
150
202
|
|
203
|
+
# Creates a Gemfile if missing
|
204
|
+
#
|
205
|
+
# @api private
|
151
206
|
def create_gemfile_if_missing
|
152
|
-
unless File.
|
153
|
-
create_file("Gemfile", %{source
|
207
|
+
unless File.exist?(File.join(destination_root, "Gemfile"))
|
208
|
+
create_file("Gemfile", %{source "https://rubygems.org"\n\n})
|
154
209
|
end
|
155
210
|
end
|
156
211
|
|
212
|
+
# Appends entries to a Gemfile.
|
213
|
+
#
|
214
|
+
# @api private
|
157
215
|
def add_gem_to_gemfile
|
158
216
|
if not_in_file?("Gemfile", %r{gem ('|")test-kitchen('|")})
|
159
|
-
append_to_file("Gemfile", %{gem
|
217
|
+
append_to_file("Gemfile", %{gem "test-kitchen"\n})
|
160
218
|
@display_bundle_msg = true
|
161
219
|
end
|
162
220
|
end
|
163
221
|
|
222
|
+
# Appends driver gems to a Gemfile or installs them.
|
223
|
+
#
|
224
|
+
# @api private
|
164
225
|
def add_drivers
|
165
226
|
return if options[:driver].nil? || options[:driver].empty?
|
166
|
-
display_warning = false
|
167
227
|
|
168
228
|
Array(options[:driver]).each do |driver_gem|
|
169
|
-
if File.
|
229
|
+
if File.exist?(File.join(destination_root, "Gemfile")) || options[:create_gemfile]
|
170
230
|
add_driver_to_gemfile(driver_gem)
|
171
231
|
else
|
172
232
|
install_gem(driver_gem)
|
@@ -174,27 +234,52 @@ module Kitchen
|
|
174
234
|
end
|
175
235
|
end
|
176
236
|
|
237
|
+
# Appends a driver gem to a Gemfile.
|
238
|
+
#
|
239
|
+
# @api private
|
177
240
|
def add_driver_to_gemfile(driver_gem)
|
178
241
|
if not_in_file?("Gemfile", %r{gem ('|")#{driver_gem}('|")})
|
179
|
-
append_to_file("Gemfile", %{gem
|
242
|
+
append_to_file("Gemfile", %{gem "#{driver_gem}"\n})
|
180
243
|
@display_bundle_msg = true
|
181
244
|
end
|
182
245
|
end
|
183
246
|
|
247
|
+
# Installs a driver gem.
|
248
|
+
#
|
249
|
+
# @api private
|
184
250
|
def install_gem(driver_gem)
|
185
|
-
unbundlerize
|
186
|
-
|
251
|
+
unbundlerize { Gem::GemRunner.new.run(["install", driver_gem]) }
|
252
|
+
rescue Gem::SystemExitException => e
|
253
|
+
raise unless e.exit_code == 0
|
254
|
+
end
|
255
|
+
|
256
|
+
# Displays a bundle warning message to the user.
|
257
|
+
#
|
258
|
+
# @api private
|
259
|
+
def display_bundle_message
|
260
|
+
if @display_bundle_msg
|
261
|
+
say "You must run `bundle install' to fetch any new gems.", :red
|
187
262
|
end
|
188
263
|
end
|
189
264
|
|
265
|
+
# Determines whether or not a pattern is found in a file.
|
266
|
+
#
|
267
|
+
# @param filename [String] filename to read
|
268
|
+
# @param regexp [Regexp] a regular expression
|
269
|
+
# @return [true,false] whether or not a pattern is found in a file
|
270
|
+
# @api private
|
190
271
|
def not_in_file?(filename, regexp)
|
191
272
|
IO.readlines(File.join(destination_root, filename)).grep(regexp).empty?
|
192
273
|
end
|
193
274
|
|
275
|
+
# Save off any Bundler/Ruby-related environment variables so that the
|
276
|
+
# yielded block can run "bundler-free" (and restore at the end).
|
277
|
+
#
|
278
|
+
# @api private
|
194
279
|
def unbundlerize
|
195
280
|
keys = ENV.keys.select { |key| key =~ /^BUNDLER?_/ } + %w[RUBYOPT]
|
196
281
|
|
197
|
-
keys.each { |key| ENV["__#{key}"] = ENV[key]
|
282
|
+
keys.each { |key| ENV["__#{key}"] = ENV[key]; ENV.delete(key) }
|
198
283
|
yield
|
199
284
|
keys.each { |key| ENV[key] = ENV.delete("__#{key}") }
|
200
285
|
end
|
data/lib/kitchen/instance.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
#
|
3
3
|
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
|
4
4
|
#
|
5
|
-
# Copyright (C) 2012, Fletcher Nichol
|
5
|
+
# Copyright (C) 2012, 2013, 2014, Fletcher Nichol
|
6
6
|
#
|
7
7
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
8
|
# you may not use this file except in compliance with the License.
|
@@ -16,8 +16,8 @@
|
|
16
16
|
# See the License for the specific language governing permissions and
|
17
17
|
# limitations under the License.
|
18
18
|
|
19
|
-
require
|
20
|
-
require
|
19
|
+
require "benchmark"
|
20
|
+
require "fileutils"
|
21
21
|
|
22
22
|
module Kitchen
|
23
23
|
|
@@ -31,10 +31,18 @@ module Kitchen
|
|
31
31
|
include Logging
|
32
32
|
|
33
33
|
class << self
|
34
|
+
|
35
|
+
# @return [Hash] a hash of mutxes, arranged by Driver class names
|
36
|
+
# @api private
|
34
37
|
attr_accessor :mutexes
|
35
38
|
|
39
|
+
# Generates a name for an instance given a suite and platform.
|
40
|
+
#
|
41
|
+
# @param suite [Suite,#name] a Suite
|
42
|
+
# @param platform [Platform,#name] a Platform
|
43
|
+
# @return [String] a normalized, consistent name for an instance
|
36
44
|
def name_for(suite, platform)
|
37
|
-
"#{suite.name}-#{platform.name}".gsub(
|
45
|
+
"#{suite.name}-#{platform.name}".gsub(%r{[_,/]}, "-").gsub(/\./, "")
|
38
46
|
end
|
39
47
|
end
|
40
48
|
|
@@ -66,11 +74,11 @@ module Kitchen
|
|
66
74
|
# Creates a new instance, given a suite and a platform.
|
67
75
|
#
|
68
76
|
# @param [Hash] options configuration for a new suite
|
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)
|
77
|
+
# @option options [Suite] :suite the suite (**Required**)
|
78
|
+
# @option options [Platform] :platform the platform (**Required**)
|
79
|
+
# @option options [Driver::Base] :driver the driver (**Required**)
|
72
80
|
# @option options [Provisioner::Base] :provisioner the provisioner
|
73
|
-
# (**Required)
|
81
|
+
# (**Required**)
|
74
82
|
# @option options [Busser] :busser the busser logger (**Required**)
|
75
83
|
# @option options [Logger] :logger the instance logger
|
76
84
|
# (default: Kitchen.logger)
|
@@ -93,6 +101,9 @@ module Kitchen
|
|
93
101
|
setup_provisioner
|
94
102
|
end
|
95
103
|
|
104
|
+
# Returns a displayable representation of the instance.
|
105
|
+
#
|
106
|
+
# @return [String] an instance display string
|
96
107
|
def to_str
|
97
108
|
"<#{name}>"
|
98
109
|
end
|
@@ -185,12 +196,24 @@ module Kitchen
|
|
185
196
|
# @see Driver::LoginCommand
|
186
197
|
# @see Driver::Base#login_command
|
187
198
|
def login
|
188
|
-
|
189
|
-
|
199
|
+
state = state_file.read
|
200
|
+
if state[:last_action].nil?
|
201
|
+
raise UserError, "Instance #{to_str} has not yet been created"
|
202
|
+
end
|
203
|
+
|
204
|
+
login_command = driver.login_command(state)
|
205
|
+
cmd, *args = login_command.cmd_array
|
190
206
|
options = login_command.options
|
191
207
|
|
192
|
-
debug(
|
193
|
-
Kernel.exec(
|
208
|
+
debug(%{Login command: #{cmd} #{args.join(" ")} (Options: #{options})})
|
209
|
+
Kernel.exec(cmd, *args, options)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Executes an arbitrary command on this instance.
|
213
|
+
#
|
214
|
+
# @param command [String] a command string to execute
|
215
|
+
def remote_exec(command)
|
216
|
+
driver.remote_command(state_file.read, command)
|
194
217
|
end
|
195
218
|
|
196
219
|
# Returns a Hash of configuration and other useful diagnostic information.
|
@@ -205,29 +228,43 @@ module Kitchen
|
|
205
228
|
result
|
206
229
|
end
|
207
230
|
|
231
|
+
# Returns the last successfully completed action state of the instance.
|
232
|
+
#
|
233
|
+
# @return [String] a named action which was last successfully completed
|
208
234
|
def last_action
|
209
235
|
state_file.read[:last_action]
|
210
236
|
end
|
211
237
|
|
212
238
|
private
|
213
239
|
|
240
|
+
# @return [StateFile] a state file object that can be read from or written
|
241
|
+
# to
|
242
|
+
# @api private
|
214
243
|
attr_reader :state_file
|
215
244
|
|
245
|
+
# Validate the initial internal state of this object and raising an
|
246
|
+
# exception if any preconditions are not met.
|
247
|
+
#
|
248
|
+
# @param options[Hash] options hash passed into the constructor
|
249
|
+
# @raise [ClientError] if any validations fail
|
250
|
+
# @api private
|
216
251
|
def validate_options(options)
|
217
|
-
[
|
218
|
-
|
219
|
-
|
220
|
-
|
252
|
+
[
|
253
|
+
:suite, :platform, :driver, :provisioner, :busser, :state_file
|
254
|
+
].each do |k|
|
255
|
+
next if options.key?(k)
|
256
|
+
|
257
|
+
raise ClientError, "Instance#new requires option :#{k}"
|
221
258
|
end
|
222
259
|
end
|
223
260
|
|
261
|
+
# Perform any final configuration or preparation needed for the driver
|
262
|
+
# object carry out its duties.
|
263
|
+
#
|
264
|
+
# @api private
|
224
265
|
def setup_driver
|
225
|
-
@driver.
|
226
|
-
@driver.validate_config!
|
227
|
-
setup_driver_mutex
|
228
|
-
end
|
266
|
+
@driver.finalize_config!(self)
|
229
267
|
|
230
|
-
def setup_driver_mutex
|
231
268
|
if driver.class.serial_actions
|
232
269
|
Kitchen.mutex.synchronize do
|
233
270
|
self.class.mutexes ||= Hash.new
|
@@ -236,10 +273,19 @@ module Kitchen
|
|
236
273
|
end
|
237
274
|
end
|
238
275
|
|
276
|
+
# Perform any final configuration or preparation needed for the provisioner
|
277
|
+
# object carry out its duties.
|
278
|
+
#
|
279
|
+
# @api private
|
239
280
|
def setup_provisioner
|
240
|
-
@provisioner.
|
281
|
+
@provisioner.finalize_config!(self)
|
241
282
|
end
|
242
283
|
|
284
|
+
# Perform all actions in order from last state to desired state.
|
285
|
+
#
|
286
|
+
# @param desired [Symbol] a symbol representing the desired action state
|
287
|
+
# @return [self] this instance, used to chain actions
|
288
|
+
# @api private
|
243
289
|
def transition_to(desired)
|
244
290
|
result = nil
|
245
291
|
FSM.actions(last_action, desired).each do |transition|
|
@@ -248,35 +294,87 @@ module Kitchen
|
|
248
294
|
result
|
249
295
|
end
|
250
296
|
|
297
|
+
# Perform the create action.
|
298
|
+
#
|
299
|
+
# @see Driver::Base#create
|
300
|
+
# @return [self] this instance, used to chain actions
|
301
|
+
# @api private
|
251
302
|
def create_action
|
252
303
|
perform_action(:create, "Creating")
|
253
304
|
end
|
254
305
|
|
306
|
+
# Perform the converge action.
|
307
|
+
#
|
308
|
+
# @see Driver::Base#converge
|
309
|
+
# @return [self] this instance, used to chain actions
|
310
|
+
# @api private
|
255
311
|
def converge_action
|
256
312
|
perform_action(:converge, "Converging")
|
257
313
|
end
|
258
314
|
|
315
|
+
# Perform the setup action.
|
316
|
+
#
|
317
|
+
# @see Driver::Base#setup
|
318
|
+
# @return [self] this instance, used to chain actions
|
319
|
+
# @api private
|
259
320
|
def setup_action
|
260
321
|
perform_action(:setup, "Setting up")
|
261
322
|
end
|
262
323
|
|
324
|
+
# Perform the verify action.
|
325
|
+
#
|
326
|
+
# @see Driver::Base#verify
|
327
|
+
# @return [self] this instance, used to chain actions
|
328
|
+
# @api private
|
263
329
|
def verify_action
|
264
330
|
perform_action(:verify, "Verifying")
|
265
331
|
end
|
266
332
|
|
333
|
+
# Perform the destroy action.
|
334
|
+
#
|
335
|
+
# @see Driver::Base#destroy
|
336
|
+
# @return [self] this instance, used to chain actions
|
337
|
+
# @api private
|
267
338
|
def destroy_action
|
268
339
|
perform_action(:destroy, "Destroying") { state_file.destroy }
|
269
340
|
end
|
270
341
|
|
342
|
+
# Perform an arbitrary action and provide useful logging.
|
343
|
+
#
|
344
|
+
# @param verb [Symbol] the action to be performed
|
345
|
+
# @param output_verb [String] a verb representing the action, suitable for
|
346
|
+
# use in output logging
|
347
|
+
# @yield perform optional work just after action has complted
|
348
|
+
# @return [self] this instance, used to chain actions
|
349
|
+
# @api private
|
271
350
|
def perform_action(verb, output_verb)
|
272
351
|
banner "#{output_verb} #{to_str}..."
|
273
352
|
elapsed = action(verb) { |state| driver.public_send(verb, state) }
|
274
|
-
info("Finished #{output_verb.downcase} #{to_str}"
|
353
|
+
info("Finished #{output_verb.downcase} #{to_str}" \
|
275
354
|
" #{Util.duration(elapsed.real)}.")
|
276
355
|
yield if block_given?
|
277
356
|
self
|
278
357
|
end
|
279
358
|
|
359
|
+
# Times a call to an action block and handles any raised exceptions. This
|
360
|
+
# method ensures that the last successfully completed action is persisted
|
361
|
+
# to the state file. The last action state will either be the desired
|
362
|
+
# action that is passed in or the previous action that was persisted to the
|
363
|
+
# state file.
|
364
|
+
#
|
365
|
+
# @param what [Symbol] the action to be performed
|
366
|
+
# @param block [Proc] a block to be called
|
367
|
+
# @return [Benchmark::Tms] timing information for the given action
|
368
|
+
# @raise [InstanceFailed] if a driver action fails to complete, signaled
|
369
|
+
# by a driver raising an ActionFailed exception. Typical reasons for this
|
370
|
+
# would be a driver create action failing, a chef convergence crashing
|
371
|
+
# in normal course of development, failing acceptance tests in the
|
372
|
+
# verify action, etc.
|
373
|
+
# @raise [ActionFailed] if an unforseen or unplanned exception is raised.
|
374
|
+
# This would usually indicate that a race condition was triggered, a
|
375
|
+
# bug exists in a driver, provisioner, or core, a transient IO error
|
376
|
+
# occured, etc.
|
377
|
+
# @api private
|
280
378
|
def action(what, &block)
|
281
379
|
state = state_file.read
|
282
380
|
elapsed = Benchmark.measure do
|
@@ -287,9 +385,9 @@ module Kitchen
|
|
287
385
|
rescue ActionFailed => e
|
288
386
|
log_failure(what, e)
|
289
387
|
raise(InstanceFailure, failure_message(what) +
|
290
|
-
" Please see .kitchen/logs/#{
|
388
|
+
" Please see .kitchen/logs/#{name}.log for more details",
|
291
389
|
e.backtrace)
|
292
|
-
rescue Exception => e
|
390
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
293
391
|
log_failure(what, e)
|
294
392
|
raise ActionFailed,
|
295
393
|
"Failed to complete ##{what} action: [#{e.message}]", e.backtrace
|
@@ -297,6 +395,16 @@ module Kitchen
|
|
297
395
|
state_file.write(state)
|
298
396
|
end
|
299
397
|
|
398
|
+
# Runs a given action block through a common driver mutex if required or
|
399
|
+
# runs it directly otherwise. If a driver class' `.serial_actions` array
|
400
|
+
# includes the desired action, then the action must be run with a muxtex
|
401
|
+
# lock. Otherwise, it is assumed that the action can happen concurrently,
|
402
|
+
# or fully in parallel.
|
403
|
+
#
|
404
|
+
# @param what [Symbol] the action to be performed
|
405
|
+
# @param state [Hash] a mutable state hash for this instance
|
406
|
+
# @param block [Proc] a block to be called
|
407
|
+
# @api private
|
300
408
|
def synchronize_or_call(what, state, &block)
|
301
409
|
if Array(driver.class.serial_actions).include?(what)
|
302
410
|
debug("#{to_str} is synchronizing on #{driver.class}##{what}")
|
@@ -309,11 +417,26 @@ module Kitchen
|
|
309
417
|
end
|
310
418
|
end
|
311
419
|
|
420
|
+
# Writes a high level message for logging and/or output.
|
421
|
+
#
|
422
|
+
# In this case, all instance banner messages will be written to the common
|
423
|
+
# Kitchen logger so that the high level flow of a run can be followed in
|
424
|
+
# the kitchen.log file.
|
425
|
+
#
|
426
|
+
# @api private
|
312
427
|
def banner(*args)
|
313
428
|
Kitchen.logger.logdev && Kitchen.logger.logdev.banner(*args)
|
314
429
|
super
|
315
430
|
end
|
316
431
|
|
432
|
+
# Logs a failure (message and backtrace) to the instance's file logger
|
433
|
+
# to help with debugging and diagnosing issues without overwhelming the
|
434
|
+
# console output in the default case (i.e. running kitchen with :info
|
435
|
+
# level debugging).
|
436
|
+
#
|
437
|
+
# @param what [String] an action
|
438
|
+
# @param e [Exception] an exception
|
439
|
+
# @api private
|
317
440
|
def log_failure(what, e)
|
318
441
|
return if logger.logdev.nil?
|
319
442
|
|
@@ -321,13 +444,20 @@ module Kitchen
|
|
321
444
|
Error.formatted_trace(e).each { |line| logger.logdev.error(line) }
|
322
445
|
end
|
323
446
|
|
447
|
+
# Returns a string explaining what action failed, at a high level. Used
|
448
|
+
# for displaying to end user.
|
449
|
+
#
|
450
|
+
# @param what [String] an action
|
451
|
+
# @return [String] a failure message
|
452
|
+
# @api private
|
324
453
|
def failure_message(what)
|
325
|
-
"#{what.capitalize} failed on instance #{
|
454
|
+
"#{what.capitalize} failed on instance #{to_str}."
|
326
455
|
end
|
327
456
|
|
328
457
|
# The simplest finite state machine pseudo-implementation needed to manage
|
329
458
|
# an Instance.
|
330
459
|
#
|
460
|
+
# @api private
|
331
461
|
# @author Fletcher Nichol <fnichol@nichol.ca>
|
332
462
|
class FSM
|
333
463
|
|
@@ -339,6 +469,7 @@ module Kitchen
|
|
339
469
|
# @param desired [String,Symbol] the desired transitioned state for the
|
340
470
|
# Instance
|
341
471
|
# @return [Array<Symbol>] an Array of transition actions to perform
|
472
|
+
# @api private
|
342
473
|
def self.actions(last = nil, desired)
|
343
474
|
last_index = index(last)
|
344
475
|
desired_index = index(desired)
|
@@ -350,10 +481,13 @@ module Kitchen
|
|
350
481
|
end
|
351
482
|
end
|
352
483
|
|
353
|
-
private
|
354
|
-
|
355
484
|
TRANSITIONS = [:destroy, :create, :converge, :setup, :verify]
|
356
485
|
|
486
|
+
# Determines the index of a state in the state lifecycle vector. Woah.
|
487
|
+
#
|
488
|
+
# @param transition [Symbol,#to_sym] a state
|
489
|
+
# @param [Integer] the index position
|
490
|
+
# @api private
|
357
491
|
def self.index(transition)
|
358
492
|
if transition.nil?
|
359
493
|
0
|