test-kitchen 1.2.1 → 1.3.0
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 +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,7 @@
|
|
16
16
|
# See the License for the specific language governing permissions and
|
17
17
|
# limitations under the License.
|
18
18
|
|
19
|
-
require
|
19
|
+
require "kitchen/command"
|
20
20
|
|
21
21
|
module Kitchen
|
22
22
|
|
@@ -27,6 +27,7 @@ module Kitchen
|
|
27
27
|
# @author Fletcher Nichol <fnichol@nichol.ca>
|
28
28
|
class Login < Kitchen::Command::Base
|
29
29
|
|
30
|
+
# Invoke the command.
|
30
31
|
def call
|
31
32
|
results = parse_subcommand(args.first)
|
32
33
|
if results.size > 1
|
data/lib/kitchen/command/sink.rb
CHANGED
@@ -16,7 +16,7 @@
|
|
16
16
|
# See the License for the specific language governing permissions and
|
17
17
|
# limitations under the License.
|
18
18
|
|
19
|
-
require
|
19
|
+
require "kitchen/command"
|
20
20
|
|
21
21
|
module Kitchen
|
22
22
|
|
@@ -27,6 +27,7 @@ module Kitchen
|
|
27
27
|
# @author Seth Vargo <sethvargo@gmail.com>
|
28
28
|
class Sink < Kitchen::Command::Base
|
29
29
|
|
30
|
+
# Invoke the command.
|
30
31
|
def call
|
31
32
|
puts [
|
32
33
|
"",
|
data/lib/kitchen/command/test.rb
CHANGED
@@ -16,9 +16,9 @@
|
|
16
16
|
# See the License for the specific language governing permissions and
|
17
17
|
# limitations under the License.
|
18
18
|
|
19
|
-
require
|
19
|
+
require "kitchen/command"
|
20
20
|
|
21
|
-
require
|
21
|
+
require "benchmark"
|
22
22
|
|
23
23
|
module Kitchen
|
24
24
|
|
@@ -31,15 +31,16 @@ module Kitchen
|
|
31
31
|
|
32
32
|
include RunAction
|
33
33
|
|
34
|
+
# Invoke the command.
|
34
35
|
def call
|
35
|
-
if
|
36
|
+
if !%w[passing always never].include?(options[:destroy])
|
36
37
|
raise ArgumentError, "Destroy mode must be passing, always, or never."
|
37
38
|
end
|
38
39
|
|
39
40
|
banner "Starting Kitchen (v#{Kitchen::VERSION})"
|
40
41
|
elapsed = Benchmark.measure do
|
41
42
|
destroy_mode = options[:destroy].to_sym
|
42
|
-
results = parse_subcommand(args.join(
|
43
|
+
results = parse_subcommand(args.join("|"))
|
43
44
|
|
44
45
|
run_action(:test, results, destroy_mode)
|
45
46
|
end
|
data/lib/kitchen/config.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.
|
@@ -20,24 +20,76 @@ module Kitchen
|
|
20
20
|
|
21
21
|
# Base configuration class for Kitchen. This class exposes configuration such
|
22
22
|
# as the location of the Kitchen config file, instances, log_levels, etc.
|
23
|
+
# This object is a factory object, meaning that it is responsible for
|
24
|
+
# consuming the desired testing configuration in and returning Ruby objects
|
25
|
+
# which are used to perfom the work.
|
26
|
+
#
|
27
|
+
# Most internal objects are created with the expectation of being
|
28
|
+
# *immutable*, meaning that internal state cannot be modified after creation.
|
29
|
+
# Any data manipulation or thread-unsafe activity is performed in this object
|
30
|
+
# so that the subsequently created objects (such as Instances, Platforms,
|
31
|
+
# Drivers, etc.) can safely run in concurrent threads of execution. To
|
32
|
+
# prevent the re-creation of duplicate objects, most created objects are
|
33
|
+
# memoized. The consequence of this is that once the Instance Array has
|
34
|
+
# been requested (with the `#instances` message), you will always be returned
|
35
|
+
# the same Instance objects.
|
36
|
+
#
|
37
|
+
# @example fetching all instances
|
38
|
+
#
|
39
|
+
# Kitchen::Config.new.instances
|
40
|
+
#
|
41
|
+
# @example fetching an instance by name
|
42
|
+
#
|
43
|
+
# Kitchen::Config.new.instances.get("default-ubuntu-12.04")
|
44
|
+
#
|
45
|
+
# @example fetching all instances matching a regular expression
|
46
|
+
#
|
47
|
+
# Kitchen::Config.new.instances.get_all(/ubuntu/)
|
23
48
|
#
|
24
49
|
# @author Fletcher Nichol <fnichol@nichol.ca>
|
25
50
|
class Config
|
26
51
|
|
52
|
+
# @return [String] the absolute path to the root of a Test Kitchen project
|
53
|
+
# @api private
|
27
54
|
attr_reader :kitchen_root
|
55
|
+
|
56
|
+
# @return [String] the absolute path to the directory into which all Test
|
57
|
+
# Kitchen log files will be written
|
58
|
+
# @api private
|
28
59
|
attr_reader :log_root
|
60
|
+
|
61
|
+
# @return [String] the absolute path to the directory containing test
|
62
|
+
# suites and other testing-related file and directories
|
63
|
+
# @api private
|
29
64
|
attr_reader :test_base_path
|
65
|
+
|
66
|
+
# @return [#read] the data loader that responds to a `#read` message,
|
67
|
+
# returning a Hash data structure
|
68
|
+
# @api private
|
30
69
|
attr_reader :loader
|
70
|
+
|
71
|
+
# @return [Symbol] the logging verbosity level
|
72
|
+
# @api private
|
31
73
|
attr_accessor :log_level
|
32
74
|
|
33
|
-
# Creates a new configuration
|
75
|
+
# Creates a new configuration, representing a particular testing
|
76
|
+
# configuration for a project.
|
34
77
|
#
|
35
78
|
# @param [Hash] options configuration
|
36
|
-
# @option options [#read] :loader
|
37
|
-
#
|
38
|
-
#
|
39
|
-
# @option options [String] :
|
40
|
-
#
|
79
|
+
# @option options [#read] :loader an object that responds to `#read` with
|
80
|
+
# a Hash structure suitable for manipulating
|
81
|
+
# (default: `Kitchen::Loader::YAML.new`)
|
82
|
+
# @option options [String] :kitchen_root an absolute path to the root of a
|
83
|
+
# Test Kitchen project, usually containing a `.kitchen.yml` file
|
84
|
+
# (default `Dir.pwd`)
|
85
|
+
# @option options [String] :log_root an absolute path to the directory
|
86
|
+
# into which all Test Kitchen log files will be written
|
87
|
+
# (default: `"#{kitchen_root}/.kitchen/logs"`)
|
88
|
+
# @option options [String] :test_base_path an absolute path to the
|
89
|
+
# directory containing test suites and other testing-related files and
|
90
|
+
# directories (default: `"#{kitchen_root}/test/integration"`)
|
91
|
+
# @option options [Symbol] :log_level the log level verbosity that the
|
92
|
+
# loggers will use when outputing information (default: `:info`)
|
41
93
|
def initialize(options = {})
|
42
94
|
@loader = options.fetch(:loader) { Kitchen::Loader::YAML.new }
|
43
95
|
@kitchen_root = options.fetch(:kitchen_root) { Dir.pwd }
|
@@ -46,20 +98,20 @@ module Kitchen
|
|
46
98
|
@test_base_path = options.fetch(:test_base_path) { default_test_base_path }
|
47
99
|
end
|
48
100
|
|
49
|
-
# @return [
|
50
|
-
# suite combinations
|
101
|
+
# @return [Collection<Instance>] all instances, resulting from all
|
102
|
+
# platform and suite combinations
|
51
103
|
def instances
|
52
104
|
@instances ||= Collection.new(build_instances)
|
53
105
|
end
|
54
106
|
|
55
|
-
# @return [
|
56
|
-
# convergence integration
|
107
|
+
# @return [Collection<Platform>] all defined platforms which will be used
|
108
|
+
# in convergence integration
|
57
109
|
def platforms
|
58
110
|
@platforms ||= Collection.new(
|
59
111
|
data.platform_data.map { |pdata| Platform.new(pdata) })
|
60
112
|
end
|
61
113
|
|
62
|
-
# @return [
|
114
|
+
# @return [Collection<Suite>] all defined suites which will be used in
|
63
115
|
# convergence integration
|
64
116
|
def suites
|
65
117
|
@suites ||= Collection.new(
|
@@ -68,24 +120,51 @@ module Kitchen
|
|
68
120
|
|
69
121
|
private
|
70
122
|
|
123
|
+
# Builds the filtered list of Instance objects.
|
124
|
+
#
|
125
|
+
# @return [Array<Instance] an array of Instances
|
126
|
+
# @api private
|
71
127
|
def build_instances
|
72
128
|
filter_instances.map.with_index do |(suite, platform), index|
|
73
129
|
new_instance(suite, platform, index)
|
74
130
|
end
|
75
131
|
end
|
76
132
|
|
133
|
+
# Returns an object which can generate configuration hashes for all the
|
134
|
+
# primary Test Kitchen objects such as Drivers, Provisioners, etc.
|
135
|
+
#
|
136
|
+
# @return [DataMunger] a data manipulator
|
137
|
+
# @api private
|
77
138
|
def data
|
78
139
|
@data ||= DataMunger.new(loader.read, kitchen_config)
|
79
140
|
end
|
80
141
|
|
142
|
+
# Determines the default absolute path to a log directory, based on the
|
143
|
+
# value of `#kitchen_root`.
|
144
|
+
#
|
145
|
+
# @return [String] an absolute path to the log directory
|
146
|
+
# @api private
|
81
147
|
def default_log_root
|
82
148
|
File.join(kitchen_root, Kitchen::DEFAULT_LOG_DIR)
|
83
149
|
end
|
84
150
|
|
151
|
+
# Determines the default absolute path to the testing files directory,
|
152
|
+
# based on the the value of `#kitchen_root`.
|
153
|
+
#
|
154
|
+
# @return [String] an absolute path to the testing files directory
|
155
|
+
# @api private
|
85
156
|
def default_test_base_path
|
86
157
|
File.join(kitchen_root, Kitchen::DEFAULT_TEST_DIR)
|
87
158
|
end
|
88
159
|
|
160
|
+
# Generates a filtered Array of tuples (Suite/Platform pairs) which is the
|
161
|
+
# cartesian product of suites and platforms. A Suite has two optional
|
162
|
+
# arrays (`#includes` and `#excludes`) which can be used to drop or
|
163
|
+
# select certain Platforms with which to join.
|
164
|
+
#
|
165
|
+
# @return [Array<Array<Suite, Platform>>] an Array of Suite/Platform
|
166
|
+
# tuples
|
167
|
+
# @api private
|
89
168
|
def filter_instances
|
90
169
|
suites.product(platforms).select do |suite, platform|
|
91
170
|
if !suite.includes.empty?
|
@@ -98,10 +177,21 @@ module Kitchen
|
|
98
177
|
end
|
99
178
|
end
|
100
179
|
|
180
|
+
# Determines the String name for an Instance, given a Suite and a Platform.
|
181
|
+
#
|
182
|
+
# @param suite [Suite,#name] a Suite
|
183
|
+
# @param platform [Platform,#name] a Platform
|
184
|
+
# @return [String] an Instance name
|
185
|
+
# @api private
|
101
186
|
def instance_name(suite, platform)
|
102
187
|
Instance.name_for(suite, platform)
|
103
188
|
end
|
104
189
|
|
190
|
+
# Generates the immutable Test Kitchen configuration and reasonable
|
191
|
+
# defaults for Drivers and Provisioners.
|
192
|
+
#
|
193
|
+
# @return [Hash] a configuration Hash
|
194
|
+
# @api private
|
105
195
|
def kitchen_config
|
106
196
|
@kitchen_config ||= {
|
107
197
|
:defaults => {
|
@@ -110,20 +200,40 @@ module Kitchen
|
|
110
200
|
},
|
111
201
|
:kitchen_root => kitchen_root,
|
112
202
|
:test_base_path => test_base_path,
|
113
|
-
:log_level => log_level
|
203
|
+
:log_level => log_level
|
114
204
|
}
|
115
205
|
end
|
116
206
|
|
207
|
+
# Builds a newly configured Busser object, for a given a Suite and Platform.
|
208
|
+
#
|
209
|
+
# @param suite [Suite,#name] a Suite
|
210
|
+
# @param platform [Platform,#name] a Platform
|
211
|
+
# @return [Busser] a new Busser object
|
212
|
+
# @api private
|
117
213
|
def new_busser(suite, platform)
|
118
214
|
bdata = data.busser_data_for(suite.name, platform.name)
|
119
215
|
Busser.new(suite.name, bdata)
|
120
216
|
end
|
121
217
|
|
218
|
+
# Builds a newly configured Driver object, for a given Suite and Platform.
|
219
|
+
#
|
220
|
+
# @param suite [Suite,#name] a Suite
|
221
|
+
# @param platform [Platform,#name] a Platform
|
222
|
+
# @return [Driver] a new Driver object
|
223
|
+
# @api private
|
122
224
|
def new_driver(suite, platform)
|
123
225
|
ddata = data.driver_data_for(suite.name, platform.name)
|
124
226
|
Driver.for_plugin(ddata[:name], ddata)
|
125
227
|
end
|
126
228
|
|
229
|
+
# Builds a newly configured Instance object, for a given Suite and
|
230
|
+
# Platform.
|
231
|
+
#
|
232
|
+
# @param suite [Suite,#name] a Suite
|
233
|
+
# @param platform [Platform,#name] a Platform
|
234
|
+
# @param index [Integer] an index used for colorizing output
|
235
|
+
# @return [Instance] a new Instance object
|
236
|
+
# @api private
|
127
237
|
def new_instance(suite, platform, index)
|
128
238
|
Instance.new(
|
129
239
|
:busser => new_busser(suite, platform),
|
@@ -136,22 +246,44 @@ module Kitchen
|
|
136
246
|
)
|
137
247
|
end
|
138
248
|
|
249
|
+
# Builds a newly configured Logger object, for a given Suite and
|
250
|
+
# Platform.
|
251
|
+
#
|
252
|
+
# @param suite [Suite,#name] a Suite
|
253
|
+
# @param platform [Platform,#name] a Platform
|
254
|
+
# @param index [Integer] an index used for colorizing output
|
255
|
+
# @return [Logger] a new Logger object
|
256
|
+
# @api private
|
139
257
|
def new_logger(suite, platform, index)
|
140
258
|
name = instance_name(suite, platform)
|
141
259
|
Logger.new(
|
142
260
|
:stdout => STDOUT,
|
143
261
|
:color => Color::COLORS[index % Color::COLORS.size].to_sym,
|
144
262
|
:logdev => File.join(log_root, "#{name}.log"),
|
145
|
-
:level => Util.to_logger_level(
|
263
|
+
:level => Util.to_logger_level(log_level),
|
146
264
|
:progname => name
|
147
265
|
)
|
148
266
|
end
|
149
267
|
|
268
|
+
# Builds a newly configured Provisioner object, for a given Suite and
|
269
|
+
# Platform.
|
270
|
+
#
|
271
|
+
# @param suite [Suite,#name] a Suite
|
272
|
+
# @param platform [Platform,#name] a Platform
|
273
|
+
# @return [Provisioner] a new Provisioner object
|
274
|
+
# @api private
|
150
275
|
def new_provisioner(suite, platform)
|
151
276
|
pdata = data.provisioner_data_for(suite.name, platform.name)
|
152
277
|
Provisioner.for_plugin(pdata[:name], pdata)
|
153
278
|
end
|
154
279
|
|
280
|
+
# Builds a newly configured StateFile object, for a given Suite and
|
281
|
+
# Platform.
|
282
|
+
#
|
283
|
+
# @param suite [Suite,#name] a Suite
|
284
|
+
# @param platform [Platform,#name] a Platform
|
285
|
+
# @return [StateFile] a new StateFile object
|
286
|
+
# @api private
|
155
287
|
def new_state_file(suite, platform)
|
156
288
|
StateFile.new(kitchen_root, instance_name(suite, platform))
|
157
289
|
end
|
@@ -0,0 +1,314 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
|
4
|
+
#
|
5
|
+
# Copyright (C) 2014, 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/util"
|
20
|
+
|
21
|
+
require "kitchen/lazy_hash"
|
22
|
+
|
23
|
+
module Kitchen
|
24
|
+
|
25
|
+
# A mixin for providing configuration-related behavior such as default
|
26
|
+
# config (static, computed, inherited), required config, local path
|
27
|
+
# expansion, etc.
|
28
|
+
#
|
29
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
30
|
+
module Configurable
|
31
|
+
|
32
|
+
def self.included(base)
|
33
|
+
base.extend(ClassMethods)
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Kitchen::Instance] the associated instance
|
37
|
+
attr_reader :instance
|
38
|
+
|
39
|
+
# A lifecycle method that should be invoked when the object is about ready
|
40
|
+
# to be used. A reference to an Instance is required as configuration
|
41
|
+
# dependant data may be access through an Instance. This also acts as a
|
42
|
+
# hook point where the object may wish to perform other last minute
|
43
|
+
# checks, validations, or configuration expansions.
|
44
|
+
#
|
45
|
+
# @param instance [Instance] an associated instance
|
46
|
+
# @return [self] itself, for use in chaining
|
47
|
+
# @raise [ClientError] if instance parameter is nil
|
48
|
+
def finalize_config!(instance)
|
49
|
+
if instance.nil?
|
50
|
+
raise ClientError, "Instance must be provided to #{self}"
|
51
|
+
end
|
52
|
+
|
53
|
+
@instance = instance
|
54
|
+
expand_paths!
|
55
|
+
validate_config!
|
56
|
+
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
# Provides hash-like access to configuration keys.
|
61
|
+
#
|
62
|
+
# @param attr [Object] configuration key
|
63
|
+
# @return [Object] value at configuration key
|
64
|
+
def [](attr)
|
65
|
+
config[attr]
|
66
|
+
end
|
67
|
+
|
68
|
+
# Find an appropriate path to a file or directory, based on graceful
|
69
|
+
# fallback rules or returns nil if path cannot be determined.
|
70
|
+
#
|
71
|
+
# Given an instance with suite named `"server"`, a `test_base_path` of
|
72
|
+
# `"/a/b"`, and a path segement of `"roles"` then following will be tried
|
73
|
+
# in order (first match that exists wins):
|
74
|
+
#
|
75
|
+
# 1. /a/b/server/roles
|
76
|
+
# 2. /a/b/roles
|
77
|
+
# 3. $PWD/roles
|
78
|
+
#
|
79
|
+
# @param path [String] the base path segment to search for
|
80
|
+
# @param opts [Hash] options
|
81
|
+
# @option opts [Symbol] :type either `:file` or `:directory` (default)
|
82
|
+
# @option opts [Symbol] :base_path a custom base path to search under,
|
83
|
+
# default uses value from `config[:test_base_path]`
|
84
|
+
# @return [String] path to the existing file or directory, or nil if file
|
85
|
+
# or directory was not found
|
86
|
+
# @raise [UserError] if `config[:test_base_path]` is used and is not set
|
87
|
+
def calculate_path(path, opts = {})
|
88
|
+
type = opts.fetch(:type, :directory)
|
89
|
+
base = opts.fetch(:base_path) do
|
90
|
+
config.fetch(:test_base_path) do |key|
|
91
|
+
raise UserError, "#{key} is not found in #{self}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
[
|
96
|
+
File.join(base, instance.suite.name, path),
|
97
|
+
File.join(base, path),
|
98
|
+
File.join(Dir.pwd, path)
|
99
|
+
].find do |candidate|
|
100
|
+
type == :directory ? File.directory?(candidate) : File.file?(candidate)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns an array of configuration keys.
|
105
|
+
#
|
106
|
+
# @return [Array] array of configuration keys
|
107
|
+
def config_keys
|
108
|
+
config.keys
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns a Hash of configuration and other useful diagnostic information.
|
112
|
+
#
|
113
|
+
# @return [Hash] a diagnostic hash
|
114
|
+
def diagnose
|
115
|
+
result = Hash.new
|
116
|
+
config_keys.sort.each { |k| result[k] = config[k] }
|
117
|
+
result
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
# @return [LzayHash] a configuration hash
|
123
|
+
# @api private
|
124
|
+
attr_reader :config
|
125
|
+
|
126
|
+
# Initializes an internal configuration hash. The hash may contain
|
127
|
+
# callable blocks as values that are meant to be called lazily. This
|
128
|
+
# method is intended to be included in an object's .initialize method.
|
129
|
+
#
|
130
|
+
# @param config [Hash] initial provided configuration
|
131
|
+
# @api private
|
132
|
+
def init_config(config)
|
133
|
+
@config = LazyHash.new(config, self)
|
134
|
+
self.class.defaults.each do |attr, value|
|
135
|
+
@config[attr] = value unless @config.key?(attr)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Expands file paths for certain configuration values. A configuration
|
140
|
+
# value is marked for file expansion with a expand_path_for declaration
|
141
|
+
# in the included class.
|
142
|
+
#
|
143
|
+
# @api private
|
144
|
+
def expand_paths!
|
145
|
+
root_path = config[:kitchen_root] || Dir.pwd
|
146
|
+
expanded_paths = LazyHash.new(self.class.expanded_paths, self).to_hash
|
147
|
+
|
148
|
+
expanded_paths.each do |key, should_expand|
|
149
|
+
next if !should_expand || config[key].nil?
|
150
|
+
|
151
|
+
config[key] = File.expand_path(config[key], root_path)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Runs all validations set up for the included class. Each validation is
|
156
|
+
# for a specific configuration attribute and has an associated callable
|
157
|
+
# block. Each validation block is called with the attribute, its value,
|
158
|
+
# and the included object for context.
|
159
|
+
#
|
160
|
+
# @api private
|
161
|
+
def validate_config!
|
162
|
+
self.class.validations.each do |attr, block|
|
163
|
+
block.call(attr, config[attr], self)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Class methods which will be mixed in on inclusion of Configurable module.
|
168
|
+
module ClassMethods
|
169
|
+
|
170
|
+
# Sets a sane default value for a configuration attribute. These values
|
171
|
+
# can be overridden by provided configuration or in a subclass with
|
172
|
+
# another default_config declaration.
|
173
|
+
#
|
174
|
+
# @example a nil default value
|
175
|
+
#
|
176
|
+
# default_config :i_am_nil
|
177
|
+
#
|
178
|
+
# @example a primitive default value
|
179
|
+
#
|
180
|
+
# default_config :use_sudo, true
|
181
|
+
#
|
182
|
+
# @example a block to compute a default value
|
183
|
+
#
|
184
|
+
# default_config :box_name do |subject|
|
185
|
+
# subject.instance.platform.name
|
186
|
+
# end
|
187
|
+
#
|
188
|
+
# @param attr [String] configuration attribute name
|
189
|
+
# @param value [Object, nil] static default value for attribute
|
190
|
+
# @yieldparam object [Object] a reference to the instantiated object
|
191
|
+
# @yieldreturn [Object, nil] dynamically computed value for the attribute
|
192
|
+
def default_config(attr, value = nil, &block)
|
193
|
+
defaults[attr] = block_given? ? block : value
|
194
|
+
end
|
195
|
+
|
196
|
+
# Ensures that an attribute which is a path will be fully expanded at
|
197
|
+
# the right time. This helps make the configuration unambiguous and much
|
198
|
+
# easier to debug and diagnose.
|
199
|
+
#
|
200
|
+
# Note that the file path expansion is only intended for paths on the
|
201
|
+
# local workstation invking the Test Kitchen code.
|
202
|
+
#
|
203
|
+
# @example the default usage
|
204
|
+
#
|
205
|
+
# expand_path_for :data_path
|
206
|
+
#
|
207
|
+
# @example disabling path expansion with a falsey value
|
208
|
+
#
|
209
|
+
# expand_path_for :relative_path, false
|
210
|
+
#
|
211
|
+
# @example using a block to determine whether or not to expand
|
212
|
+
#
|
213
|
+
# expand_path_for :relative_or_not_path do |subject|
|
214
|
+
# subject.instance.name =~ /default/
|
215
|
+
# end
|
216
|
+
#
|
217
|
+
# @param attr [String] configuration attribute name
|
218
|
+
# @param value [Object, nil] whether or not to exand the file path
|
219
|
+
# @yieldparam object [Object] a reference to the instantiated object
|
220
|
+
# @yieldreturn [Object, nil] dynamically compute whether or not to
|
221
|
+
# perform the file expansion
|
222
|
+
def expand_path_for(attr, value = true, &block)
|
223
|
+
expanded_paths[attr] = block_given? ? block : value
|
224
|
+
end
|
225
|
+
|
226
|
+
# Ensures that an attribute must have a non-nil, non-empty String value.
|
227
|
+
# The default behavior will be to raise a user error and thereby halting
|
228
|
+
# further configuration processing. Good use cases for require_config
|
229
|
+
# might be cloud provider credential keys and other similar data.
|
230
|
+
#
|
231
|
+
# @example a value that must not be nil or an empty String
|
232
|
+
#
|
233
|
+
# required_config :cloud_api_token
|
234
|
+
#
|
235
|
+
# @example using a block to use custom validation logic
|
236
|
+
#
|
237
|
+
# required_config :email do |attr, value, subject|
|
238
|
+
# raise UserError, "Must be an email address" unless value =~ /@/
|
239
|
+
# end
|
240
|
+
#
|
241
|
+
# @param attr [String] configuration attribute name
|
242
|
+
# @yieldparam attr [Symbol] the attribute name
|
243
|
+
# @yieldparam value [Object] the current value of the attribute
|
244
|
+
# @yieldparam object [Object] a reference to the instantiated object
|
245
|
+
def required_config(attr, &block)
|
246
|
+
if !block_given?
|
247
|
+
klass = self
|
248
|
+
block = lambda do |_, value, thing|
|
249
|
+
if value.nil? || value.to_s.empty?
|
250
|
+
attribute = "#{klass}#{thing.instance.to_str}#config[:#{attr}]"
|
251
|
+
raise UserError, "#{attribute} cannot be blank"
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
validations[attr] = block
|
256
|
+
end
|
257
|
+
|
258
|
+
# @return [Hash] a hash of attribute keys and default values which has
|
259
|
+
# been merged with any superclass defaults
|
260
|
+
# @api private
|
261
|
+
def defaults
|
262
|
+
@defaults ||= Hash.new.merge(super_defaults)
|
263
|
+
end
|
264
|
+
|
265
|
+
# @return [Hash] a hash of defaults from the included class' superclass
|
266
|
+
# if defined in the superclass, or an empty hash otherwise
|
267
|
+
# @api private
|
268
|
+
def super_defaults
|
269
|
+
if superclass.respond_to?(:defaults)
|
270
|
+
superclass.defaults
|
271
|
+
else
|
272
|
+
Hash.new
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
# @return [Hash] a hash of attribute keys and truthy/falsey values to
|
277
|
+
# determine if said attribute needs to be fully file path expanded,
|
278
|
+
# which has been merged with any superclass expanded paths
|
279
|
+
# @api private
|
280
|
+
def expanded_paths
|
281
|
+
@expanded_paths ||= Hash.new.merge(super_expanded_paths)
|
282
|
+
end
|
283
|
+
|
284
|
+
# @return [Hash] a hash of expanded paths from the included class'
|
285
|
+
# superclass if defined in the superclass, or an empty hash otherwise
|
286
|
+
# @api private
|
287
|
+
def super_expanded_paths
|
288
|
+
if superclass.respond_to?(:expanded_paths)
|
289
|
+
superclass.expanded_paths
|
290
|
+
else
|
291
|
+
Hash.new
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# @return [Hash] a hash of attribute keys and valudation callable blocks
|
296
|
+
# which has been merged with any superclass valudations
|
297
|
+
# @api private
|
298
|
+
def validations
|
299
|
+
@validations ||= Hash.new.merge(super_validations)
|
300
|
+
end
|
301
|
+
|
302
|
+
# @return [Hash] a hash of validations from the included class'
|
303
|
+
# superclass if defined in the superclass, or an empty hash otherwise
|
304
|
+
# @api private
|
305
|
+
def super_validations
|
306
|
+
if superclass.respond_to?(:validations)
|
307
|
+
superclass.validations
|
308
|
+
else
|
309
|
+
Hash.new
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|