test-kitchen 1.0.0.alpha.7 → 1.0.0.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +70 -2
- data/README.md +2 -2
- data/Rakefile +14 -11
- data/lib/kitchen.rb +7 -2
- data/lib/kitchen/busser.rb +19 -7
- data/lib/kitchen/cli.rb +29 -7
- data/lib/kitchen/color.rb +5 -5
- data/lib/kitchen/config.rb +49 -10
- data/lib/kitchen/driver.rb +4 -0
- data/lib/kitchen/driver/base.rb +42 -25
- data/lib/kitchen/driver/ssh_base.rb +45 -139
- data/lib/kitchen/errors.rb +3 -0
- data/lib/kitchen/generator/init.rb +41 -31
- data/lib/kitchen/instance.rb +34 -20
- data/lib/kitchen/login_command.rb +34 -0
- data/lib/kitchen/platform.rb +22 -12
- data/lib/kitchen/provisioner.rb +50 -0
- data/lib/kitchen/provisioner/base.rb +63 -0
- data/lib/kitchen/provisioner/chef_base.rb +290 -0
- data/lib/kitchen/provisioner/chef_solo.rb +69 -0
- data/lib/kitchen/provisioner/chef_zero.rb +92 -0
- data/lib/kitchen/ssh.rb +160 -0
- data/lib/kitchen/suite.rb +53 -33
- data/lib/kitchen/version.rb +1 -1
- data/spec/kitchen/config_spec.rb +40 -8
- data/spec/kitchen/driver/base_spec.rb +123 -0
- data/spec/kitchen/instance_spec.rb +61 -54
- data/spec/kitchen/platform_spec.rb +20 -13
- data/spec/kitchen/suite_spec.rb +34 -28
- data/support/chef-client-zero.rb +59 -0
- data/templates/init/kitchen.yml.erb +4 -4
- data/test-kitchen.gemspec +1 -1
- metadata +55 -90
- data/lib/kitchen/chef_data_uploader.rb +0 -177
data/lib/kitchen/driver.rb
CHANGED
@@ -20,6 +20,10 @@ require 'thor/util'
|
|
20
20
|
|
21
21
|
module Kitchen
|
22
22
|
|
23
|
+
# A driver is responsible for carrying out the lifecycle activities of an
|
24
|
+
# instance, such as creating, converging, and destroying an instance.
|
25
|
+
#
|
26
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
23
27
|
module Driver
|
24
28
|
|
25
29
|
# Returns an instance of a driver given a plugin type string.
|
data/lib/kitchen/driver/base.rb
CHANGED
@@ -22,23 +22,7 @@ module Kitchen
|
|
22
22
|
|
23
23
|
module Driver
|
24
24
|
|
25
|
-
#
|
26
|
-
# for execution.
|
27
|
-
#
|
28
|
-
# @author Fletcher Nichol <fnichol@nichol.ca>
|
29
|
-
class LoginCommand
|
30
|
-
|
31
|
-
attr_reader :cmd_array, :options
|
32
|
-
|
33
|
-
def initialize(cmd_array, options = {})
|
34
|
-
@cmd_array = cmd_array
|
35
|
-
@options = options
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
# Base class for a driver. A driver is responsible for carrying out the
|
40
|
-
# lifecycle activities of an instance, such as creating, converging, and
|
41
|
-
# destroying an instance.
|
25
|
+
# Base class for a driver.
|
42
26
|
#
|
43
27
|
# @author Fletcher Nichol <fnichol@nichol.ca>
|
44
28
|
class Base
|
@@ -46,22 +30,32 @@ module Kitchen
|
|
46
30
|
include ShellOut
|
47
31
|
include Logging
|
48
32
|
|
49
|
-
|
33
|
+
attr_accessor :instance
|
50
34
|
|
51
35
|
class << self
|
52
36
|
attr_reader :serial_actions
|
53
37
|
end
|
54
38
|
|
55
39
|
def initialize(config = {})
|
56
|
-
@config = config
|
40
|
+
@config = LazyDriverHash.new(config, self)
|
57
41
|
self.class.defaults.each do |attr, value|
|
58
42
|
@config[attr] = value unless @config[attr]
|
59
43
|
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate_config!
|
60
47
|
Array(self.class.validations).each do |tuple|
|
61
|
-
tuple.last.call(tuple.first, config[tuple.first])
|
48
|
+
tuple.last.call(tuple.first, config[tuple.first], self)
|
62
49
|
end
|
63
50
|
end
|
64
51
|
|
52
|
+
# Returns the name of this driver, suitable for display in a CLI.
|
53
|
+
#
|
54
|
+
# @return [String] name of this driver
|
55
|
+
def name
|
56
|
+
self.class.name.split('::').last
|
57
|
+
end
|
58
|
+
|
65
59
|
# Provides hash-like access to configuration keys.
|
66
60
|
#
|
67
61
|
# @param attr [Object] configuration key
|
@@ -121,7 +115,7 @@ module Kitchen
|
|
121
115
|
|
122
116
|
protected
|
123
117
|
|
124
|
-
attr_reader :config
|
118
|
+
attr_reader :config
|
125
119
|
|
126
120
|
ACTION_METHODS = %w{create converge setup verify destroy}.
|
127
121
|
map(&:to_sym).freeze
|
@@ -180,8 +174,8 @@ module Kitchen
|
|
180
174
|
end
|
181
175
|
end
|
182
176
|
|
183
|
-
def self.default_config(attr, value)
|
184
|
-
defaults[attr] = value
|
177
|
+
def self.default_config(attr, value = nil, &block)
|
178
|
+
defaults[attr] = block_given? ? block : value
|
185
179
|
end
|
186
180
|
|
187
181
|
def self.validations
|
@@ -192,9 +186,10 @@ module Kitchen
|
|
192
186
|
@validations = [] if @validations.nil?
|
193
187
|
if ! block_given?
|
194
188
|
klass = self
|
195
|
-
block = lambda do |attr, value|
|
189
|
+
block = lambda do |attr, value, driver|
|
196
190
|
if value.nil? || value.to_s.empty?
|
197
|
-
|
191
|
+
attribute = "#{klass}#{driver.instance.to_str}#config[:#{attr}]"
|
192
|
+
raise UserError, "#{attribute} cannot be blank"
|
198
193
|
end
|
199
194
|
end
|
200
195
|
end
|
@@ -211,6 +206,28 @@ module Kitchen
|
|
211
206
|
@serial_actions ||= []
|
212
207
|
@serial_actions += methods
|
213
208
|
end
|
209
|
+
|
210
|
+
# A modifed Hash object that may contain procs as a value which must be
|
211
|
+
# executed in the context of a Driver object.
|
212
|
+
#
|
213
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
214
|
+
class LazyDriverHash < SimpleDelegator
|
215
|
+
|
216
|
+
def initialize(obj, driver)
|
217
|
+
@driver = driver
|
218
|
+
super(obj)
|
219
|
+
end
|
220
|
+
|
221
|
+
def [](key)
|
222
|
+
proc_or_val = __getobj__[key]
|
223
|
+
|
224
|
+
if proc_or_val.respond_to?(:call)
|
225
|
+
proc_or_val.call(@driver)
|
226
|
+
else
|
227
|
+
proc_or_val
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
214
231
|
end
|
215
232
|
end
|
216
233
|
end
|
@@ -16,9 +16,6 @@
|
|
16
16
|
# See the License for the specific language governing permissions and
|
17
17
|
# limitations under the License.
|
18
18
|
|
19
|
-
require 'net/ssh'
|
20
|
-
require 'socket'
|
21
|
-
|
22
19
|
module Kitchen
|
23
20
|
|
24
21
|
module Driver
|
@@ -39,28 +36,29 @@ module Kitchen
|
|
39
36
|
end
|
40
37
|
|
41
38
|
def converge(state)
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
39
|
+
provisioner = new_provisioner
|
40
|
+
|
41
|
+
Kitchen::SSH.new(*build_ssh_args(state)) do |conn|
|
42
|
+
run_remote(provisioner.install_command, conn)
|
43
|
+
run_remote(provisioner.init_command, conn)
|
44
|
+
transfer_path(provisioner.create_sandbox, provisioner.home_path, conn)
|
45
|
+
run_remote(provisioner.prepare_command, conn)
|
46
|
+
run_remote(provisioner.run_command, conn)
|
47
|
+
end
|
48
|
+
ensure
|
49
|
+
provisioner && provisioner.cleanup_sandbox
|
48
50
|
end
|
49
51
|
|
50
52
|
def setup(state)
|
51
|
-
|
52
|
-
|
53
|
-
if busser_setup_cmd
|
54
|
-
ssh(ssh_args, busser_setup_cmd)
|
53
|
+
Kitchen::SSH.new(*build_ssh_args(state)) do |conn|
|
54
|
+
run_remote(busser_setup_cmd, conn)
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
58
|
def verify(state)
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
ssh(ssh_args, busser_sync_cmd)
|
63
|
-
ssh(ssh_args, busser_run_cmd)
|
59
|
+
Kitchen::SSH.new(*build_ssh_args(state)) do |conn|
|
60
|
+
run_remote(busser_sync_cmd, conn)
|
61
|
+
run_remote(busser_run_cmd, conn)
|
64
62
|
end
|
65
63
|
end
|
66
64
|
|
@@ -69,20 +67,23 @@ module Kitchen
|
|
69
67
|
end
|
70
68
|
|
71
69
|
def login_command(state)
|
72
|
-
|
73
|
-
|
74
|
-
args = %W{ -o UserKnownHostsFile=/dev/null }
|
75
|
-
args += %W{ -o StrictHostKeyChecking=no }
|
76
|
-
args += %W{ -o LogLevel=#{logger.debug? ? "VERBOSE" : "ERROR"} }
|
77
|
-
args += %W{ -i #{combined[:ssh_key]}} if combined[:ssh_key]
|
78
|
-
args += %W{ -p #{combined[:port]}} if combined[:port]
|
79
|
-
args += %W{ #{combined[:username]}@#{combined[:hostname]}}
|
70
|
+
SSH.new(*build_ssh_args(state)).login_command
|
71
|
+
end
|
80
72
|
|
81
|
-
|
73
|
+
def ssh(ssh_args, command)
|
74
|
+
Kitchen::SSH.new(*ssh_args) do |conn|
|
75
|
+
run_remote(command, conn)
|
76
|
+
end
|
82
77
|
end
|
83
78
|
|
84
79
|
protected
|
85
80
|
|
81
|
+
def new_provisioner
|
82
|
+
combined = config.dup
|
83
|
+
combined[:log_level] = Util.from_logger_level(logger.level)
|
84
|
+
Provisioner.for_plugin(combined[:provisioner], instance, combined)
|
85
|
+
end
|
86
|
+
|
86
87
|
def build_ssh_args(state)
|
87
88
|
combined = config.merge(state)
|
88
89
|
|
@@ -92,132 +93,37 @@ module Kitchen
|
|
92
93
|
opts[:password] = combined[:password] if combined[:password]
|
93
94
|
opts[:port] = combined[:port] if combined[:port]
|
94
95
|
opts[:keys] = Array(combined[:ssh_key]) if combined[:ssh_key]
|
96
|
+
opts[:logger] = logger
|
95
97
|
|
96
98
|
[combined[:hostname], combined[:username], opts]
|
97
99
|
end
|
98
100
|
|
99
|
-
def
|
100
|
-
"/tmp/kitchen-chef-solo".freeze
|
101
|
-
end
|
102
|
-
|
103
|
-
def install_omnibus(ssh_args)
|
104
|
-
url = "https://www.opscode.com/chef/install.sh"
|
105
|
-
flag = config[:require_chef_omnibus]
|
106
|
-
version = if flag.is_a?(String) && flag != "latest"
|
107
|
-
"-s -- -v #{flag.downcase}"
|
108
|
-
else
|
109
|
-
""
|
110
|
-
end
|
111
|
-
|
112
|
-
ssh(ssh_args, <<-INSTALL.gsub(/^ {10}/, ''))
|
113
|
-
should_update_chef() {
|
114
|
-
case "#{flag}" in
|
115
|
-
true|$(chef-solo -v | cut -d " " -f 2)) return 1 ;;
|
116
|
-
latest|*) return 0 ;;
|
117
|
-
esac
|
118
|
-
}
|
119
|
-
|
120
|
-
if [ ! -d "/opt/chef" ] || should_update_chef ; then
|
121
|
-
echo "-----> Installing Chef Omnibus (#{flag})"
|
122
|
-
if command -v wget >/dev/null ; then
|
123
|
-
wget #{url} -O - | #{cmd('bash')} #{version}
|
124
|
-
elif command -v curl >/dev/null ; then
|
125
|
-
curl -sSL #{url} | #{cmd('bash')} #{version}
|
126
|
-
else
|
127
|
-
echo ">>>>>> Neither wget nor curl found on this instance."
|
128
|
-
exit 1
|
129
|
-
fi
|
130
|
-
fi
|
131
|
-
INSTALL
|
132
|
-
end
|
133
|
-
|
134
|
-
def prepare_chef_home(ssh_args)
|
135
|
-
ssh(ssh_args, "#{cmd('rm')} -rf #{chef_home} && mkdir -p #{chef_home}/cache")
|
136
|
-
end
|
137
|
-
|
138
|
-
def upload_chef_data(ssh_args)
|
139
|
-
Kitchen::ChefDataUploader.new(
|
140
|
-
instance, ssh_args, config[:kitchen_root], chef_home
|
141
|
-
).upload
|
142
|
-
end
|
143
|
-
|
144
|
-
def run_chef_solo(ssh_args)
|
145
|
-
ssh(ssh_args, <<-RUN_SOLO)
|
146
|
-
#{cmd('chef-solo')} -c #{chef_home}/solo.rb -j #{chef_home}/dna.json \
|
147
|
-
--log_level #{Util.from_logger_level(logger.level)}
|
148
|
-
RUN_SOLO
|
149
|
-
end
|
150
|
-
|
151
|
-
def ssh(ssh_args, cmd)
|
101
|
+
def env_cmd(cmd)
|
152
102
|
env = "env"
|
153
|
-
if config[:http_proxy]
|
154
|
-
|
155
|
-
end
|
156
|
-
if config[:https_proxy]
|
157
|
-
env << " https_proxy=#{config[:https_proxy]}"
|
158
|
-
end
|
159
|
-
if env != "env"
|
160
|
-
cmd = "#{env} #{cmd}"
|
161
|
-
end
|
162
|
-
|
163
|
-
debug("[SSH] #{ssh_args[1]}@#{ssh_args[0]} (#{cmd})")
|
164
|
-
Net::SSH.start(*ssh_args) do |ssh|
|
165
|
-
exit_code = ssh_exec_with_exit!(ssh, cmd)
|
103
|
+
env << " http_proxy=#{config[:http_proxy]}" if config[:http_proxy]
|
104
|
+
env << " https_proxy=#{config[:https_proxy]}" if config[:https_proxy]
|
166
105
|
|
167
|
-
|
168
|
-
shorter_cmd = cmd.squeeze(" ").strip
|
169
|
-
raise ActionFailed,
|
170
|
-
"SSH exited (#{exit_code}) for command: [#{shorter_cmd}]"
|
171
|
-
end
|
172
|
-
end
|
173
|
-
rescue Net::SSH::Exception => ex
|
174
|
-
raise ActionFailed, ex.message
|
106
|
+
env == "env" ? cmd : "#{env} #{cmd}"
|
175
107
|
end
|
176
108
|
|
177
|
-
def
|
178
|
-
|
179
|
-
ssh.open_channel do |channel|
|
109
|
+
def run_remote(command, connection)
|
110
|
+
return if command.nil?
|
180
111
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
channel.on_data do |ch, data|
|
186
|
-
logger << data
|
187
|
-
end
|
188
|
-
|
189
|
-
channel.on_extended_data do |ch, type, data|
|
190
|
-
logger << data
|
191
|
-
end
|
192
|
-
|
193
|
-
channel.on_request("exit-status") do |ch, data|
|
194
|
-
exit_code = data.read_long
|
195
|
-
end
|
196
|
-
end
|
197
|
-
end
|
198
|
-
ssh.loop
|
199
|
-
exit_code
|
112
|
+
connection.exec(env_cmd(command))
|
113
|
+
rescue SSHFailed, Net::SSH::Exception => ex
|
114
|
+
raise ActionFailed, ex.message
|
200
115
|
end
|
201
116
|
|
202
|
-
def
|
203
|
-
|
204
|
-
end
|
117
|
+
def transfer_path(local, remote, connection)
|
118
|
+
return if local.nil?
|
205
119
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
rescue SocketError, Errno::ECONNREFUSED,
|
210
|
-
Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
|
211
|
-
sleep 2
|
212
|
-
false
|
213
|
-
rescue Errno::EPERM, Errno::ETIMEDOUT
|
214
|
-
false
|
215
|
-
ensure
|
216
|
-
socket && socket.close
|
120
|
+
connection.upload_path!(local, remote)
|
121
|
+
rescue SSHFailed, Net::SSH::Exception => ex
|
122
|
+
raise ActionFailed, ex.message
|
217
123
|
end
|
218
124
|
|
219
|
-
def
|
220
|
-
|
125
|
+
def wait_for_sshd(*ssh_args)
|
126
|
+
SSH.new(*ssh_args).wait
|
221
127
|
end
|
222
128
|
end
|
223
129
|
end
|
data/lib/kitchen/errors.rb
CHANGED
@@ -47,33 +47,12 @@ module Kitchen
|
|
47
47
|
self.class.source_root(Kitchen.source_root.join("templates", "init"))
|
48
48
|
|
49
49
|
create_kitchen_yaml
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
begin
|
54
|
-
require 'kitchen/rake_tasks'
|
55
|
-
Kitchen::RakeTasks.new
|
56
|
-
rescue LoadError
|
57
|
-
puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV['CI']
|
58
|
-
end
|
59
|
-
RAKE
|
60
|
-
append_to_file("Rakefile", rakedoc) if init_rakefile?
|
61
|
-
|
62
|
-
thordoc = <<-THOR.gsub(/^ {10}/, '')
|
63
|
-
|
64
|
-
begin
|
65
|
-
require 'kitchen/thor_tasks'
|
66
|
-
Kitchen::ThorTasks.new
|
67
|
-
rescue LoadError
|
68
|
-
puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV['CI']
|
69
|
-
end
|
70
|
-
THOR
|
71
|
-
append_to_file("Thorfile", thordoc) if init_thorfile?
|
72
|
-
|
50
|
+
prepare_rakefile if init_rakefile?
|
51
|
+
prepare_thorfile if init_thorfile?
|
73
52
|
empty_directory "test/integration/default" if init_test_dir?
|
74
53
|
append_to_gitignore(".kitchen/")
|
75
54
|
append_to_gitignore(".kitchen.local.yml")
|
76
|
-
prepare_gemfile if
|
55
|
+
prepare_gemfile if init_gemfile?
|
77
56
|
add_drivers
|
78
57
|
|
79
58
|
if @display_bundle_msg
|
@@ -98,13 +77,18 @@ module Kitchen
|
|
98
77
|
})
|
99
78
|
end
|
100
79
|
|
80
|
+
def init_gemfile?
|
81
|
+
File.exists?(File.join(destination_root, "Gemfile")) ||
|
82
|
+
options[:create_gemfile]
|
83
|
+
end
|
84
|
+
|
101
85
|
def init_rakefile?
|
102
|
-
File.exists?("Rakefile") &&
|
86
|
+
File.exists?(File.join(destination_root, "Rakefile")) &&
|
103
87
|
not_in_file?("Rakefile", %r{require 'kitchen/rake_tasks'})
|
104
88
|
end
|
105
89
|
|
106
90
|
def init_thorfile?
|
107
|
-
File.exists?("Thorfile") &&
|
91
|
+
File.exists?(File.join(destination_root, "Thorfile")) &&
|
108
92
|
not_in_file?("Thorfile", %r{require 'kitchen/thor_tasks'})
|
109
93
|
end
|
110
94
|
|
@@ -112,10 +96,36 @@ module Kitchen
|
|
112
96
|
Dir.glob("test/integration/*").select { |d| File.directory?(d) }.empty?
|
113
97
|
end
|
114
98
|
|
99
|
+
def prepare_rakefile
|
100
|
+
rakedoc = <<-RAKE.gsub(/^ {10}/, '')
|
101
|
+
|
102
|
+
begin
|
103
|
+
require 'kitchen/rake_tasks'
|
104
|
+
Kitchen::RakeTasks.new
|
105
|
+
rescue LoadError
|
106
|
+
puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV['CI']
|
107
|
+
end
|
108
|
+
RAKE
|
109
|
+
append_to_file(File.join(destination_root, "Rakefile"), rakedoc)
|
110
|
+
end
|
111
|
+
|
112
|
+
def prepare_thorfile
|
113
|
+
thordoc = <<-THOR.gsub(/^ {10}/, '')
|
114
|
+
|
115
|
+
begin
|
116
|
+
require 'kitchen/thor_tasks'
|
117
|
+
Kitchen::ThorTasks.new
|
118
|
+
rescue LoadError
|
119
|
+
puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV['CI']
|
120
|
+
end
|
121
|
+
THOR
|
122
|
+
append_to_file(File.join(destination_root, "Thorfile"), thordoc)
|
123
|
+
end
|
124
|
+
|
115
125
|
def append_to_gitignore(line)
|
116
|
-
create_file(".gitignore") unless File.exists?(".gitignore")
|
126
|
+
create_file(".gitignore") unless File.exists?(File.join(destination_root, ".gitignore"))
|
117
127
|
|
118
|
-
if IO.readlines(".gitignore").grep(%r{^#{line}}).empty?
|
128
|
+
if IO.readlines(File.join(destination_root, ".gitignore")).grep(%r{^#{line}}).empty?
|
119
129
|
append_to_file(".gitignore", "#{line}\n")
|
120
130
|
end
|
121
131
|
end
|
@@ -126,7 +136,7 @@ module Kitchen
|
|
126
136
|
end
|
127
137
|
|
128
138
|
def create_gemfile_if_missing
|
129
|
-
unless File.exists?("Gemfile")
|
139
|
+
unless File.exists?(File.join(destination_root, "Gemfile"))
|
130
140
|
create_file("Gemfile", %{source 'https://rubygems.org'\n\n})
|
131
141
|
end
|
132
142
|
end
|
@@ -144,7 +154,7 @@ module Kitchen
|
|
144
154
|
display_warning = false
|
145
155
|
|
146
156
|
Array(options[:driver]).each do |driver_gem|
|
147
|
-
if File.exists?("Gemfile") || options[:create_gemfile]
|
157
|
+
if File.exists?(File.join(destination_root, "Gemfile")) || options[:create_gemfile]
|
148
158
|
add_driver_to_gemfile(driver_gem)
|
149
159
|
else
|
150
160
|
install_gem(driver_gem)
|
@@ -167,7 +177,7 @@ module Kitchen
|
|
167
177
|
end
|
168
178
|
|
169
179
|
def not_in_file?(filename, regexp)
|
170
|
-
IO.readlines(filename).grep(regexp).empty?
|
180
|
+
IO.readlines(File.join(destination_root, filename)).grep(regexp).empty?
|
171
181
|
end
|
172
182
|
|
173
183
|
def unbundlerize
|