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.
- 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
|