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.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +1 -1
  3. data/.rubocop.yml +3 -0
  4. data/.travis.yml +20 -9
  5. data/CHANGELOG.md +219 -108
  6. data/Gemfile +10 -6
  7. data/Guardfile +38 -9
  8. data/README.md +11 -1
  9. data/Rakefile +21 -37
  10. data/bin/kitchen +4 -4
  11. data/features/kitchen_action_commands.feature +161 -0
  12. data/features/kitchen_console_command.feature +34 -0
  13. data/features/kitchen_diagnose_command.feature +64 -0
  14. data/features/kitchen_init_command.feature +29 -17
  15. data/features/kitchen_list_command.feature +2 -2
  16. data/features/kitchen_login_command.feature +56 -0
  17. data/features/{sink_command.feature → kitchen_sink_command.feature} +0 -0
  18. data/features/kitchen_test_command.feature +88 -0
  19. data/features/step_definitions/gem_steps.rb +8 -6
  20. data/features/step_definitions/git_steps.rb +4 -2
  21. data/features/step_definitions/output_steps.rb +5 -0
  22. data/features/support/env.rb +12 -9
  23. data/lib/kitchen.rb +60 -38
  24. data/lib/kitchen/base64_stream.rb +55 -0
  25. data/lib/kitchen/busser.rb +124 -58
  26. data/lib/kitchen/cli.rb +121 -38
  27. data/lib/kitchen/collection.rb +3 -3
  28. data/lib/kitchen/color.rb +4 -4
  29. data/lib/kitchen/command.rb +78 -11
  30. data/lib/kitchen/command/action.rb +3 -2
  31. data/lib/kitchen/command/console.rb +12 -5
  32. data/lib/kitchen/command/diagnose.rb +17 -3
  33. data/lib/kitchen/command/driver_discover.rb +26 -7
  34. data/lib/kitchen/command/exec.rb +41 -0
  35. data/lib/kitchen/command/list.rb +44 -14
  36. data/lib/kitchen/command/login.rb +2 -1
  37. data/lib/kitchen/command/sink.rb +2 -1
  38. data/lib/kitchen/command/test.rb +5 -4
  39. data/lib/kitchen/config.rb +146 -14
  40. data/lib/kitchen/configurable.rb +314 -0
  41. data/lib/kitchen/data_munger.rb +522 -18
  42. data/lib/kitchen/diagnostic.rb +43 -4
  43. data/lib/kitchen/driver.rb +4 -4
  44. data/lib/kitchen/driver/base.rb +80 -115
  45. data/lib/kitchen/driver/dummy.rb +34 -6
  46. data/lib/kitchen/driver/proxy.rb +14 -3
  47. data/lib/kitchen/driver/ssh_base.rb +61 -7
  48. data/lib/kitchen/errors.rb +109 -9
  49. data/lib/kitchen/generator/driver_create.rb +39 -5
  50. data/lib/kitchen/generator/init.rb +130 -45
  51. data/lib/kitchen/instance.rb +162 -28
  52. data/lib/kitchen/lazy_hash.rb +79 -7
  53. data/lib/kitchen/loader/yaml.rb +159 -27
  54. data/lib/kitchen/logger.rb +267 -21
  55. data/lib/kitchen/logging.rb +30 -3
  56. data/lib/kitchen/login_command.rb +11 -2
  57. data/lib/kitchen/metadata_chopper.rb +2 -2
  58. data/lib/kitchen/provisioner.rb +4 -4
  59. data/lib/kitchen/provisioner/base.rb +107 -103
  60. data/lib/kitchen/provisioner/chef/berkshelf.rb +36 -8
  61. data/lib/kitchen/provisioner/chef/librarian.rb +40 -11
  62. data/lib/kitchen/provisioner/chef_base.rb +206 -167
  63. data/lib/kitchen/provisioner/chef_solo.rb +25 -7
  64. data/lib/kitchen/provisioner/chef_zero.rb +105 -29
  65. data/lib/kitchen/provisioner/dummy.rb +1 -1
  66. data/lib/kitchen/provisioner/shell.rb +21 -6
  67. data/lib/kitchen/rake_tasks.rb +8 -3
  68. data/lib/kitchen/shell_out.rb +15 -18
  69. data/lib/kitchen/ssh.rb +122 -27
  70. data/lib/kitchen/state_file.rb +24 -7
  71. data/lib/kitchen/thor_tasks.rb +9 -4
  72. data/lib/kitchen/util.rb +43 -118
  73. data/lib/kitchen/version.rb +1 -1
  74. data/lib/vendor/hash_recursive_merge.rb +10 -2
  75. data/spec/kitchen/base64_stream_spec.rb +77 -0
  76. data/spec/kitchen/busser_spec.rb +490 -0
  77. data/spec/kitchen/collection_spec.rb +10 -10
  78. data/spec/kitchen/color_spec.rb +2 -2
  79. data/spec/kitchen/config_spec.rb +234 -62
  80. data/spec/kitchen/configurable_spec.rb +490 -0
  81. data/spec/kitchen/data_munger_spec.rb +1070 -862
  82. data/spec/kitchen/diagnostic_spec.rb +79 -0
  83. data/spec/kitchen/driver/base_spec.rb +80 -85
  84. data/spec/kitchen/driver/dummy_spec.rb +43 -14
  85. data/spec/kitchen/driver/proxy_spec.rb +134 -0
  86. data/spec/kitchen/driver/ssh_base_spec.rb +644 -0
  87. data/spec/kitchen/driver_spec.rb +15 -15
  88. data/spec/kitchen/errors_spec.rb +309 -0
  89. data/spec/kitchen/instance_spec.rb +143 -46
  90. data/spec/kitchen/lazy_hash_spec.rb +36 -9
  91. data/spec/kitchen/loader/yaml_spec.rb +237 -226
  92. data/spec/kitchen/logger_spec.rb +419 -0
  93. data/spec/kitchen/logging_spec.rb +59 -0
  94. data/spec/kitchen/login_command_spec.rb +49 -0
  95. data/spec/kitchen/metadata_chopper_spec.rb +82 -0
  96. data/spec/kitchen/platform_spec.rb +4 -4
  97. data/spec/kitchen/provisioner/base_spec.rb +65 -125
  98. data/spec/kitchen/provisioner/chef_base_spec.rb +798 -0
  99. data/spec/kitchen/provisioner/chef_solo_spec.rb +316 -0
  100. data/spec/kitchen/provisioner/chef_zero_spec.rb +624 -0
  101. data/spec/kitchen/provisioner/shell_spec.rb +269 -0
  102. data/spec/kitchen/provisioner_spec.rb +6 -6
  103. data/spec/kitchen/shell_out_spec.rb +143 -0
  104. data/spec/kitchen/ssh_spec.rb +683 -0
  105. data/spec/kitchen/state_file_spec.rb +28 -21
  106. data/spec/kitchen/suite_spec.rb +7 -7
  107. data/spec/kitchen/util_spec.rb +68 -10
  108. data/spec/kitchen_spec.rb +107 -0
  109. data/spec/spec_helper.rb +18 -13
  110. data/support/chef-client-zero.rb +10 -9
  111. data/support/chef_helpers.sh +16 -0
  112. data/support/download_helpers.sh +109 -0
  113. data/test-kitchen.gemspec +42 -33
  114. metadata +107 -33
@@ -18,12 +18,39 @@
18
18
 
19
19
  module Kitchen
20
20
 
21
+ # Mixin module that delegates logging methods to a local `#logger`.
22
+ #
23
+ # @author Fletcher Nichol <fnichol@nichol.ca>
21
24
  module Logging
22
25
 
23
- %w{banner debug info warn error fatal}.map(&:to_sym).each do |meth|
24
- define_method(meth) do |*args|
25
- logger.public_send(meth, *args)
26
+ class << self
27
+
28
+ private
29
+
30
+ # @api private
31
+ # @!macro logger_method
32
+ # @method $1($2)
33
+ # Log a message with severity of $1
34
+ # @param message_or_progname [#to_s] the message to log. In the block
35
+ # form, this is the progname to use in the log message.
36
+ # @yield evaluates to the message to log. This is not evaluated unless
37
+ # the logger's level is sufficient to log the message. This allows
38
+ # you to create potentially expensive logging messages that are
39
+ # only called when the logger is configured to show them.
40
+ # @return [nil,true] when the given severity is not high enough (for
41
+ # this particular logger), log no message, and return true
42
+ def logger_method(meth)
43
+ define_method(meth) do |*args|
44
+ logger.public_send(meth, *args)
45
+ end
26
46
  end
27
47
  end
48
+
49
+ logger_method :banner
50
+ logger_method :debug
51
+ logger_method :info
52
+ logger_method :warn
53
+ logger_method :error
54
+ logger_method :fatal
28
55
  end
29
56
  end
@@ -24,10 +24,19 @@ module Kitchen
24
24
  # @author Fletcher Nichol <fnichol@nichol.ca>
25
25
  class LoginCommand
26
26
 
27
- attr_reader :cmd_array, :options
27
+ # @return [Array] array of login command arguments
28
+ attr_reader :cmd_array
28
29
 
30
+ # @return [Hash] options hash, passed to `Kernel#exec`
31
+ attr_reader :options
32
+
33
+ # Constructs a new LoginCommand instance.
34
+ #
35
+ # @param cmd_array [Array] array of login command arguments
36
+ # @param options [Hash] options hash, passed to `Kernel#exec`
37
+ # @see http://www.ruby-doc.org/core-2.1.2/Kernel.html#method-i-exec
29
38
  def initialize(cmd_array, options = {})
30
- @cmd_array = cmd_array
39
+ @cmd_array = Array(cmd_array)
31
40
  @options = options
32
41
  end
33
42
  end
@@ -42,10 +42,10 @@ module Kitchen
42
42
  #
43
43
  # @param metadata_file [String] path to a metadata.rb file
44
44
  def initialize(metadata_file)
45
- eval(IO.read(metadata_file), nil, metadata_file)
45
+ instance_eval(IO.read(metadata_file), metadata_file)
46
46
  end
47
47
 
48
- def method_missing(meth, *args, &block)
48
+ def method_missing(meth, *args, &_block)
49
49
  self[meth] = args.first
50
50
  end
51
51
  end
@@ -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 'thor/util'
19
+ require "thor/util"
20
20
 
21
21
  module Kitchen
22
22
 
@@ -40,12 +40,12 @@ module Kitchen
40
40
  require("kitchen/provisioner/#{plugin}")
41
41
 
42
42
  str_const = Thor::Util.camel_case(plugin)
43
- klass = self.const_get(str_const)
43
+ klass = const_get(str_const)
44
44
  klass.new(config)
45
45
  rescue LoadError, NameError
46
46
  raise ClientError,
47
- "Could not load the '#{plugin}' provisioner from the load path." +
48
- " Please ensure that your provisioner is installed as a gem or" +
47
+ "Could not load the '#{plugin}' provisioner from the load path." \
48
+ " Please ensure that your provisioner is installed as a gem or" \
49
49
  " included in your Gemfile if using Bundler."
50
50
  end
51
51
  end
@@ -16,8 +16,6 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
- require 'kitchen/lazy_hash'
20
-
21
19
  module Kitchen
22
20
 
23
21
  module Provisioner
@@ -27,53 +25,95 @@ module Kitchen
27
25
  # @author Fletcher Nichol <fnichol@nichol.ca>
28
26
  class Base
29
27
 
28
+ include Configurable
30
29
  include Logging
31
30
 
32
- attr_accessor :instance
31
+ default_config :root_path, "/tmp/kitchen"
32
+ default_config :sudo, true
33
33
 
34
+ expand_path_for :test_base_path
35
+
36
+ # Constructs a new provisioner by providing a configuration hash.
37
+ #
38
+ # @param config [Hash] initial provided configuration
34
39
  def initialize(config = {})
35
- @config = LazyHash.new(config, self)
36
- self.class.defaults.each do |attr, value|
37
- @config[attr] = value unless @config.has_key?(attr)
38
- end
40
+ init_config(config)
39
41
  end
40
42
 
41
- def instance=(instance)
42
- @instance = instance
43
- expand_paths!
43
+ # Performs any final configuration required for the provisioner to do its
44
+ # work. A reference to an Instance is required as configuration dependant
45
+ # data may need access through an Instance. This also acts as a hook
46
+ # point where the object may wish to perform other last minute checks,
47
+ # valiations, or configuration expansions.
48
+ #
49
+ # @param instance [Instance] an associated instance
50
+ # @return [self] itself, used for chaining
51
+ # @raise [ClientError] if instance parameter is nil
52
+ def finalize_config!(instance)
53
+ super
44
54
  load_needed_dependencies!
55
+ self
45
56
  end
46
57
 
47
58
  # Returns the name of this driver, suitable for display in a CLI.
48
59
  #
49
60
  # @return [String] name of this driver
50
61
  def name
51
- self.class.name.split('::').last
62
+ self.class.name.split("::").last
52
63
  end
53
64
 
54
- # Provides hash-like access to configuration keys.
65
+ # Generates a command string which will install and configure the
66
+ # provisioner software on an instance. If no work is required, then `nil`
67
+ # will be returned.
55
68
  #
56
- # @param attr [Object] configuration key
57
- # @return [Object] value at configuration key
58
- def [](attr)
59
- config[attr]
69
+ # @return [String] a command string
70
+ def install_command
60
71
  end
61
72
 
62
- # Returns an array of configuration keys.
73
+ # Generates a command string which will perform any data initialization
74
+ # or configuration required after the provisioner software is installed
75
+ # but before the sandbox has been transferred to the instance. If no work
76
+ # is required, then `nil` will be returned.
63
77
  #
64
- # @return [Array] array of configuration keys
65
- def config_keys
66
- config.keys
78
+ # @return [String] a command string
79
+ def init_command
67
80
  end
68
81
 
69
- def init_command ; end
70
-
71
- def install_command ; end
72
-
73
- def prepare_command ; end
82
+ # Generates a command string which will perform any commands or
83
+ # configuration required just before the main provisioner run command but
84
+ # after the sandbox has been transferred to the instance. If no work is
85
+ # required, then `nil` will be returned.
86
+ #
87
+ # @return [String] a command string
88
+ def prepare_command
89
+ end
74
90
 
75
- def run_command ; end
91
+ # Generates a command string which will invoke the main provisioner
92
+ # command on the prepared instance. If no work is required, then `nil`
93
+ # will be returned.
94
+ #
95
+ # @return [String] a command string
96
+ def run_command
97
+ end
76
98
 
99
+ # Creates a temporary directory on the local workstation into which
100
+ # provisioner related files and directories can be copied or created. The
101
+ # contents of this directory will be copied over to the instance before
102
+ # invoking the provisioner's run command. After this method completes, it
103
+ # is expected that the contents of the sandbox is complete and ready for
104
+ # copy to the remote instance.
105
+ #
106
+ # **Note:** any subclasses would be well advised to call super first when
107
+ # overriding this method, for example:
108
+ #
109
+ # @example overriding `#create_sandbox`
110
+ #
111
+ # class MyProvisioner < Kitchen::Provisioner::Base
112
+ # def create_sandbox
113
+ # super
114
+ # # any further file copies, preparations, etc.
115
+ # end
116
+ # end
77
117
  def create_sandbox
78
118
  @sandbox_path = Dir.mktmpdir("#{instance.name}-sandbox-")
79
119
  File.chmod(0755, sandbox_path)
@@ -81,12 +121,21 @@ module Kitchen
81
121
  debug("Creating local sandbox in #{sandbox_path}")
82
122
  end
83
123
 
124
+ # Returns the absolute path to the sandbox directory or raises an
125
+ # exception if `#create_sandbox` has not yet been called.
126
+ #
127
+ # @return [String] the absolute path to the sandbox directory
128
+ # @raise [ClientError] if the sandbox directory has no yet been created
129
+ # by calling `#create_sandbox`
84
130
  def sandbox_path
85
- @sandbox_path || (raise ClientError, "Sandbox directory has not yet " +
86
- "been created. Please run #{self.class}#create_sandox before " +
131
+ @sandbox_path || (raise ClientError, "Sandbox directory has not yet " \
132
+ "been created. Please run #{self.class}#create_sandox before " \
87
133
  "trying to access the path.")
88
134
  end
89
135
 
136
+ # Deletes the sandbox path. Without calling this method, the sandbox path
137
+ # will persist after the process terminates. In other words, cleanup is
138
+ # explicit. This method is safe to call multiple times.
90
139
  def cleanup_sandbox
91
140
  return if sandbox_path.nil?
92
141
 
@@ -94,91 +143,46 @@ module Kitchen
94
143
  FileUtils.rmtree(sandbox_path)
95
144
  end
96
145
 
97
- # Returns a Hash of configuration and other useful diagnostic information.
98
- #
99
- # @return [Hash] a diagnostic hash
100
- def diagnose
101
- result = Hash.new
102
- config_keys.sort.each { |k| result[k] = config[k] }
103
- result
104
- end
105
-
106
- def calculate_path(path, type = :directory)
107
- base = config[:test_base_path]
108
- candidates = []
109
- candidates << File.join(base, instance.suite.name, path)
110
- candidates << File.join(base, path)
111
- candidates << File.join(Dir.pwd, path)
112
-
113
- candidates.find do |c|
114
- type == :directory ? File.directory?(c) : File.file?(c)
115
- end
116
- end
117
-
118
- protected
119
-
120
- attr_reader :config
121
-
122
- def expand_paths!
123
- expanded_paths = LazyHash.new(self.class.expanded_paths, self).to_hash
146
+ private
124
147
 
125
- expanded_paths.each do |key, should_expand|
126
- if should_expand && !config[key].nil?
127
- config[key] = File.expand_path(config[key], config[:kitchen_root])
128
- end
129
- end
148
+ # Loads any required third party Ruby libraries or runs any shell out
149
+ # commands to prepare the provisioner. This method will be called in the
150
+ # context of the main thread of execution and so does not necessarily
151
+ # have to be thread safe.
152
+ #
153
+ # **Note:** any subclasses overriding this method would be well advised
154
+ # to call super when overriding this method, for example:
155
+ #
156
+ # @example overriding `#load_needed_dependencies!`
157
+ #
158
+ # class MyProvisioner < Kitchen::Provisioner::Base
159
+ # def load_needed_dependencies!
160
+ # super
161
+ # # any further work
162
+ # end
163
+ # end
164
+ #
165
+ # @raise [ClientError] if any library loading fails or any of the
166
+ # dependency requirements cannot be satisfied
167
+ # @api private
168
+ def load_needed_dependencies!
130
169
  end
131
170
 
132
- def load_needed_dependencies! ; end
133
-
171
+ # @return [Logger] the instance's logger or Test Kitchen's common logger
172
+ # otherwise
173
+ # @api private
134
174
  def logger
135
175
  instance ? instance.logger : Kitchen.logger
136
176
  end
137
177
 
178
+ # Conditionally prefixes a command with a sudo command.
179
+ #
180
+ # @param command [String] command to be prefixed
181
+ # @return [String] the command, conditionaly prefixed with sudo
182
+ # @api private
138
183
  def sudo(script)
139
184
  config[:sudo] ? "sudo -E #{script}" : script
140
185
  end
141
-
142
- def self.defaults
143
- @defaults ||= Hash.new.merge(super_defaults)
144
- end
145
-
146
- def self.super_defaults
147
- klass = self.superclass
148
-
149
- if klass.respond_to?(:defaults)
150
- klass.defaults
151
- else
152
- Hash.new
153
- end
154
- end
155
-
156
- def self.default_config(attr, value = nil, &block)
157
- defaults[attr] = block_given? ? block : value
158
- end
159
-
160
- def self.expanded_paths
161
- @expanded_paths ||= Hash.new.merge(super_expanded_paths)
162
- end
163
-
164
- def self.super_expanded_paths
165
- klass = self.superclass
166
-
167
- if klass.respond_to?(:expanded_paths)
168
- klass.expanded_paths
169
- else
170
- Hash.new
171
- end
172
- end
173
-
174
- def self.expand_path_for(attr, value = true, &block)
175
- expanded_paths[attr] = block_given? ? block : value
176
- end
177
-
178
- default_config :root_path, "/tmp/kitchen"
179
- default_config :sudo, true
180
-
181
- expand_path_for :test_base_path
182
186
  end
183
187
  end
184
188
  end
@@ -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 'kitchen/errors'
20
- require 'kitchen/logging'
19
+ require "kitchen/errors"
20
+ require "kitchen/logging"
21
21
 
22
22
  module Kitchen
23
23
 
@@ -33,16 +33,29 @@ module Kitchen
33
33
 
34
34
  include Logging
35
35
 
36
+ # Creates a new cookbook resolver.
37
+ #
38
+ # @param berksfile [String] path to a Berksfile
39
+ # @param path [String] path in which to vendor the resulting
40
+ # cookbooks
41
+ # @param logger [Kitchen::Logger] a logger to use for output, defaults
42
+ # to `Kitchen.logger`
36
43
  def initialize(berksfile, path, logger = Kitchen.logger)
37
44
  @berksfile = berksfile
38
45
  @path = path
39
46
  @logger = logger
40
47
  end
41
48
 
49
+ # Loads the library code required to use the resolver.
50
+ #
51
+ # @param logger [Kitchen::Logger] a logger to use for output, defaults
52
+ # to `Kitchen.logger`
42
53
  def self.load!(logger = Kitchen.logger)
43
54
  load_berkshelf!(logger)
44
55
  end
45
56
 
57
+ # Performs the cookbook resolution and vendors the resulting cookbooks
58
+ # in the desired path.
46
59
  def resolve
47
60
  version = ::Berkshelf::VERSION
48
61
  info("Resolving cookbook dependencies with Berkshelf #{version}...")
@@ -54,17 +67,32 @@ module Kitchen
54
67
  FileUtils.rm_rf(path)
55
68
  ::Berkshelf::Berksfile.from_file(berksfile).vendor(path)
56
69
  else
57
- ::Berkshelf::Berksfile.from_file(berksfile).install(path: path)
70
+ ::Berkshelf::Berksfile.from_file(berksfile).install(:path => path)
58
71
  end
59
72
  end
60
73
  end
61
74
 
62
75
  private
63
76
 
64
- attr_reader :berksfile, :path, :logger
77
+ # @return [String] path to a Berksfile
78
+ # @api private
79
+ attr_reader :berksfile
65
80
 
81
+ # @return [String] path in which to vendor the resulting cookbooks
82
+ # @api private
83
+ attr_reader :path
84
+
85
+ # @return [Kitchen::Logger] a logger to use for output
86
+ # @api private
87
+ attr_reader :logger
88
+
89
+ # Load the Berkshelf-specific libary code.
90
+ #
91
+ # @param logger [Kitchen::Logger] the logger to use
92
+ # @raise [UserError] if the library couldn't be loaded
93
+ # @api private
66
94
  def self.load_berkshelf!(logger)
67
- first_load = require 'berkshelf'
95
+ first_load = require "berkshelf"
68
96
 
69
97
  version = ::Berkshelf::VERSION
70
98
  if first_load
@@ -73,9 +101,9 @@ module Kitchen
73
101
  logger.debug("Berkshelf #{version} previously loaded")
74
102
  end
75
103
  rescue LoadError => e
76
- logger.fatal("The `berkshelf' gem is missing and must be installed" +
77
- " or cannot be properly activated. Run" +
78
- " `gem install berkshelf` or add the following to your" +
104
+ logger.fatal("The `berkshelf' gem is missing and must be installed" \
105
+ " or cannot be properly activated. Run" \
106
+ " `gem install berkshelf` or add the following to your" \
79
107
  " Gemfile if you are using Bundler: `gem 'berkshelf'`.")
80
108
  raise UserError,
81
109
  "Could not load or activate Berkshelf (#{e.message})"