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.
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})"