diff options
35 files changed, 3735 insertions, 557 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index a3605e3084..df15462289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 12.5.0 +* [**Ranjib Dey**](https://github.com/ranjib): + [pr#3588](https://github.com/chef/chef/pull/3588) Count skipped resources among total resources in doc formatter + ## 12.4.0 * [**Phil Dibowitz**](https://github.com/jaymzh): diff --git a/DOC_CHANGES.md b/DOC_CHANGES.md index 46712f114c..b1121e2bf4 100644 --- a/DOC_CHANGES.md +++ b/DOC_CHANGES.md @@ -6,79 +6,3 @@ Example Doc Change: Description of the required change. --> -### Resources now *all* get automatic DSL - -When you declare a resource (no matter where) you now get automatic DSL for it, based on your class name: - -```ruby -module MyModule - class MyResource < Chef::Resource - # Names the resource "my_resource" - end -end -``` - -When this happens, the resource can be used in a recipe: - -```ruby -my_resource 'blah' do -end -``` - -If you have an abstract class that should *not* have DSL, set `resource_name` to `nil`: - -```ruby -module MyModule - # This will not have DSL - class MyBaseResource < Chef::Resource - resource_name nil - end - # This will have DSL `my_resource` - class MyResource < MyBaseResource - end -end -``` - -When you do this, `my_base_resource` will not work in a recipe (but `my_resource` will). - -You can still use `provides` to provide other DSL names: - -```ruby -module MyModule - class MyResource < Chef::Resource - provides :super_resource - end -end -``` - -Which enables this recipe: - -```ruby -super_resource 'wowzers' do -end -``` - -(Note that when you use provides in this manner, resource_name will be `my_resource` and declared_type will be `super_resource`. This won't affect most people, but it is worth noting as a matter of explanation.) - -Users are encouraged to declare resources in their own namespaces instead of putting them in the `Chef::Resource` namespace. - -### Resources may now use `allowed_actions` and `default_action` - -Instead of overriding `Chef::Resource.initialize` and setting `@allowed_actions` and `@action` in the constructor, you may now use the `allowed_actions` and `default_action` DSL to declare them: - -```ruby -class MyResource < Chef::Resource - allowed_actions :create, :delete - default_action :create -end -``` - -### LWRPs are no longer automatically placed in the `Chef::Resource` namespace - -Starting with Chef 12.4.0, accessing an LWRP class by name from the `Chef::Resource` namespace will trigger a deprecation warning message. This means that if your cookbook includes the LWRP `mycookbook/resources/myresource.rb`, you will no longer be able to extend or reference `Chef::Resource::MycookbookMyresource` in Ruby code. LWRP recipe DSL does not change: the LWRP will still be available to recipes as `mycookbook_myresource`. - -You can still get the LWRP class by calling `Chef::ResourceResolver.resolve(:mycookbook_myresource)`. - -The primary aim here is clearing out the `Chef::Resource` namespace. - -References to these classes is deprecated (and will emit a warning) in Chef 12, and will be removed in Chef 13. diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 9cc74f6fa8..43009d1548 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -35,6 +35,7 @@ another component. * [Jay Mundrawala](https://github.com/jdmundrawala) * [Jon Cowie](https://github.com/jonlives) * [Lamont Granquist](https://github.com/lamont-granquist) +* [Claire McQuin](https://github.com/mcquin) * [Steven Murawski](https://github.com/smurawski) * [Tyler Ball](https://github.com/tyler-ball) * [Ranjib Dey](https://github.com/ranjib) diff --git a/MAINTAINERS.toml b/MAINTAINERS.toml index 4b55ae2798..8f36763e54 100644 --- a/MAINTAINERS.toml +++ b/MAINTAINERS.toml @@ -40,6 +40,7 @@ another component. "jdmundrawala", "jonlives", "lamont-granquist", + "mcquin", "smurawski", "tyler-ball", "ranjib" diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 049640d7ab..93a759ba88 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,87 +1 @@ -# Chef Client Release Notes 12.4.0: - -## Knife Key Management Commands for Users and Clients - -`knife user` and `knife client` now have a suite of subcommands that live under -the `key` subcommand. These subcommands allow you to list, show, create, delete -and edit keys for a given user or client. They can be used to implement -key rotation with multiple expiring keys for a single actor or just -for basic key management. See `knife user key` and `knife client key` -for a full list of subcommands and their usage. - -## System Loggers - -You can now have all Chef logs sent to a logging system of your choice. - -### Syslog Logger - -Syslog can be used by adding the following line to your chef config -file: - -```ruby -log_location Chef::Log::Syslog.new("chef-client", ::Syslog::LOG_DAEMON) -``` - -THis will write to the `daemon` facility with the originator set as -`chef-client`. - -### Windows Event Logger - -The logger can be used by adding the following line to your chef config file: - -```ruby -log_location Chef::Log::WinEvt.new -``` - -This will write to the Application log with the source set as Chef. - -## RemoteFile resource supports UNC paths on Windows - -You can now use UNC paths with `remote_file` on Windows machines. For -example, you can get `Foo.tar.gz` off of `fooshare` on `foohost` using -the following resource: - -```ruby -remote_file 'C:\Foo.tar.gz' do - source "\\\\foohost\\fooshare\\Foo.tar.gz" -end -``` - -## WindowsPackage resource supports URLs - -The `windows_package` resource now allows specifying URLs for the source -attribute. For example, you could install 7zip with the following resource: - -```ruby -windows_package '7zip' do - source "http://www.7-zip.org/a/7z938-x64.msi" -end -``` - -Internally, this is done by using a `remote_file` resource to download the -contents at the specified url. If needed, you can modify the attributes of -the `remote_file` resource using the `remote_file_attributes` attribute. -The `remote_file_attributes` accepts a hash of attributes that will be set -on the underlying remote_file. For example, the checksum of the contents can -be verified using - -```ruby -windows_package '7zip' do - source "http://www.7-zip.org/a/7z938-x64.msi" - remote_file_attributes { - :path => "C:\\7zip.msi", - :checksum => '7c8e873991c82ad9cfcdbdf45254ea6101e9a645e12977dcd518979e50fdedf3' - } -end -``` - -To make the transition easier from the Windows cookbook, `windows_package` also -accepts the `checksum` attribute, and the previous resource could be rewritten -as: - -```ruby -windows_package '7zip' do - source "http://www.7-zip.org/a/7z938-x64.msi" - checksum '7c8e873991c82ad9cfcdbdf45254ea6101e9a645e12977dcd518979e50fdedf3' -end -``` +# Chef Client Release Notes 12.5.0: @@ -1 +1 @@ -12.4.0.rc.2 +12.5.0.current.0 diff --git a/chef-config/lib/chef-config/version.rb b/chef-config/lib/chef-config/version.rb index a6bf636540..aee626f240 100644 --- a/chef-config/lib/chef-config/version.rb +++ b/chef-config/lib/chef-config/version.rb @@ -20,6 +20,6 @@ #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! module ChefConfig - VERSION = '12.4.0.rc.2' + VERSION = '12.5.0.current.0' end diff --git a/kitchen-tests/Gemfile b/kitchen-tests/Gemfile index 988d876417..5e1907ba1f 100644 --- a/kitchen-tests/Gemfile +++ b/kitchen-tests/Gemfile @@ -6,4 +6,5 @@ group :end_to_end do gem 'kitchen-appbundle-updater', '~> 0.0.1' gem "kitchen-vagrant", '~> 0.17.0' gem 'kitchen-ec2', github: 'test-kitchen/kitchen-ec2' + gem 'vagrant-wrapper' end diff --git a/lib/chef/client.rb b/lib/chef/client.rb index 86e92585e3..3c86f52b4a 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -309,12 +309,18 @@ class Chef # with the proper exit status code and everything gets raised # as a RunFailedWrappingError if run_error || converge_error || audit_error - error = if run_error == converge_error - Chef::Exceptions::RunFailedWrappingError.new(converge_error, audit_error) - else - Chef::Exceptions::RunFailedWrappingError.new(run_error, converge_error, audit_error) - end - error.fill_backtrace + error = if Chef::Config[:audit_mode] == :disabled + run_error || converge_error + else + e = if run_error == converge_error + Chef::Exceptions::RunFailedWrappingError.new(converge_error, audit_error) + else + Chef::Exceptions::RunFailedWrappingError.new(run_error, converge_error, audit_error) + end + e.fill_backtrace + e + end + Chef::Application.debug_stacktrace(error) raise error end diff --git a/lib/chef/dsl/recipe.rb b/lib/chef/dsl/recipe.rb index d69f0a8f11..d00d0df247 100644 --- a/lib/chef/dsl/recipe.rb +++ b/lib/chef/dsl/recipe.rb @@ -122,9 +122,9 @@ class Chef def describe_self_for_error if respond_to?(:name) - %Q[`#{self.class.name} "#{name}"'] + %Q[`#{self.class} "#{name}"'] elsif respond_to?(:recipe_name) - %Q[`#{self.class.name} "#{recipe_name}"'] + %Q[`#{self.class} "#{recipe_name}"'] else to_s end @@ -176,6 +176,24 @@ class Chef raise NameError, "No resource, method, or local variable named `#{method_symbol}' for #{describe_self_for_error}" end end + + module FullDSL + require 'chef/dsl/data_query' + require 'chef/dsl/platform_introspection' + require 'chef/dsl/include_recipe' + require 'chef/dsl/registry_helper' + require 'chef/dsl/reboot_pending' + require 'chef/dsl/audit' + require 'chef/dsl/powershell' + include Chef::DSL::DataQuery + include Chef::DSL::PlatformIntrospection + include Chef::DSL::IncludeRecipe + include Chef::DSL::Recipe + include Chef::DSL::RegistryHelper + include Chef::DSL::RebootPending + include Chef::DSL::Audit + include Chef::DSL::Powershell + end end end end diff --git a/lib/chef/event_dispatch/base.rb b/lib/chef/event_dispatch/base.rb index 73fe25ec13..50aee63450 100644 --- a/lib/chef/event_dispatch/base.rb +++ b/lib/chef/event_dispatch/base.rb @@ -269,26 +269,37 @@ class Chef # def notifications_resolved # end + # + # Resource events and ordering: + # + # 1. Start the action + # - resource_action_start + # 2. Check the guard + # - resource_skipped: (goto 7) if only_if/not_if say to skip + # 3. Load the current resource + # - resource_current_state_loaded + # - resource_current_state_load_bypassed (if not why-run safe) + # 4. Check if why-run safe + # - resource_bypassed: (goto 7) if not why-run safe + # 5. During processing: + # - resource_update_applied: For each actual change (many per action) + # 6. Processing complete status: + # - resource_failed if the resource threw an exception while running + # - resource_failed_retriable: (goto 3) if resource failed and will be retried + # - resource_updated if the resource was updated (resource_update_applied will have been called) + # - resource_up_to_date if the resource was up to date (no resource_update_applied) + # 7. Processing complete: + # - resource_completed + # + # Called before action is executed on a resource. def resource_action_start(resource, action, notification_type=nil, notifier=nil) end - # Called when a resource fails, but will retry. - def resource_failed_retriable(resource, action, retry_count, exception) - end - - # Called when a resource fails and will not be retried. - def resource_failed(resource, action, exception) - end - # Called when a resource action has been skipped b/c of a conditional def resource_skipped(resource, action, conditional) end - # Called when a resource action has been completed - def resource_completed(resource) - end - # Called after #load_current_resource has run. def resource_current_state_loaded(resource, action, current_resource) end @@ -302,21 +313,34 @@ class Chef def resource_bypassed(resource, action, current_resource) end - # Called when a resource has no converge actions, e.g., it was already correct. - def resource_up_to_date(resource, action) - end - # Called when a change has been made to a resource. May be called multiple # times per resource, e.g., a file may have its content updated, and then # its permissions updated. def resource_update_applied(resource, action, update) end + # Called when a resource fails, but will retry. + def resource_failed_retriable(resource, action, retry_count, exception) + end + + # Called when a resource fails and will not be retried. + def resource_failed(resource, action, exception) + end + # Called after a resource has been completely converged, but only if # modifications were made. def resource_updated(resource, action) end + # Called when a resource has no converge actions, e.g., it was already correct. + def resource_up_to_date(resource, action) + end + + # Called when a resource action has been completed + def resource_completed(resource) + end + + # A stream has opened. def stream_opened(stream, options = {}) end diff --git a/lib/chef/formatters/doc.rb b/lib/chef/formatters/doc.rb index e76a940c38..5ea9823d78 100644 --- a/lib/chef/formatters/doc.rb +++ b/lib/chef/formatters/doc.rb @@ -22,6 +22,7 @@ class Chef @failed_audits = 0 @start_time = Time.now @end_time = @start_time + @skipped_resources = 0 end def elapsed_time @@ -33,7 +34,7 @@ class Chef end def total_resources - @up_to_date_resources + @updated_resources + @up_to_date_resources + @updated_resources + @skipped_resources end def total_audits @@ -236,6 +237,7 @@ class Chef # Called when a resource action has been skipped b/c of a conditional def resource_skipped(resource, action, conditional) + @skipped_resources += 1 # TODO: more info about conditional puts " (skipped due to #{conditional.short_description})", :stream => resource unindent diff --git a/lib/chef/mixin/params_validate.rb b/lib/chef/mixin/params_validate.rb index 78d72dc801..f7d52a19cf 100644 --- a/lib/chef/mixin/params_validate.rb +++ b/lib/chef/mixin/params_validate.rb @@ -16,6 +16,8 @@ # limitations under the License. class Chef + NOT_PASSED = Object.new + class DelayedEvaluator < Proc end module Mixin @@ -65,7 +67,7 @@ class Chef true when Hash validation.each do |check, carg| - check_method = "_pv_#{check.to_s}" + check_method = "_pv_#{check}" if self.respond_to?(check_method, true) self.send(check_method, opts, key, carg) else @@ -81,162 +83,407 @@ class Chef DelayedEvaluator.new(&block) end - def set_or_return(symbol, arg, validation) - iv_symbol = "@#{symbol.to_s}".to_sym - if arg == nil && self.instance_variable_defined?(iv_symbol) == true - ivar = self.instance_variable_get(iv_symbol) - if(ivar.is_a?(DelayedEvaluator)) - validate({ symbol => ivar.call }, { symbol => validation })[symbol] - else - ivar - end + def set_or_return(symbol, value, validation) + symbol = symbol.to_sym + iv_symbol = :"@#{symbol}" + + # Steal default, coerce, name_property and required from validation + # so that we can handle the order in which they are applied + validation = validation.dup + if validation.has_key?(:default) + default = validation.delete(:default) + elsif validation.has_key?('default') + default = validation.delete('default') else - if(arg.is_a?(DelayedEvaluator)) - val = arg + default = NOT_PASSED + end + coerce = validation.delete(:coerce) + coerce ||= validation.delete('coerce') + name_property = validation.delete(:name_property) + name_property ||= validation.delete('name_property') + name_property ||= validation.delete(:name_attribute) + name_property ||= validation.delete('name_attribute') + required = validation.delete(:required) + required ||= validation.delete('required') + + opts = {} + # If the user passed NOT_PASSED, or passed nil, then this is a get. + if value == NOT_PASSED || (value.nil? && !explicitly_allows_nil?(symbol, validation)) + + # Get the value if there is one + if self.instance_variable_defined?(iv_symbol) + opts[symbol] = self.instance_variable_get(iv_symbol) + + # Handle lazy values + if opts[symbol].is_a?(DelayedEvaluator) + if opts[symbol].arity >= 1 + opts[symbol] = opts[symbol].call(self) + else + opts[symbol] = opts[symbol].call + end + + # Coerce and validate the default value + _pv_required(opts, symbol, required, explicitly_allows_nil?(symbol, validation)) if required + _pv_coerce(opts, symbol, coerce) if coerce + validate(opts, { symbol => validation }) + end + + # Get the default value else - val = validate({ symbol => arg }, { symbol => validation })[symbol] + _pv_required(opts, symbol, required, explicitly_allows_nil?(symbol, validation)) if required + _pv_default(opts, symbol, default) unless default == NOT_PASSED + _pv_name_property(opts, symbol, name_property) + + if opts.has_key?(symbol) + # Handle lazy defaults. + if opts[symbol].is_a?(DelayedEvaluator) + if opts[symbol].arity >= 1 + opts[symbol] = opts[symbol].call(self) + else + opts[symbol] = instance_eval(&opts[symbol]) + end + end - # Handle the case where the "default" was a DelayedEvaluator. In - # this case, the block yields an optional parameter of +self+, - # which is the equivalent of "new_resource" - if val.is_a?(DelayedEvaluator) - val = val.call(self) + # Coerce and validate the default value + _pv_required(opts, symbol, required, explicitly_allows_nil?(symbol, validation)) if required + _pv_coerce(opts, symbol, coerce) if coerce + # We presently do not validate defaults, for backwards compatibility. +# validate(opts, { symbol => validation }) + + # Defaults are presently "stickily" set on the instance + self.instance_variable_set(iv_symbol, opts[symbol]) end end - self.instance_variable_set(iv_symbol, val) + + # Set the value + else + opts[symbol] = value + unless opts[symbol].is_a?(DelayedEvaluator) + # Coerce and validate the value + _pv_required(opts, symbol, required, explicitly_allows_nil?(symbol, validation)) if required + _pv_coerce(opts, symbol, coerce) if coerce + validate(opts, { symbol => validation }) + end + + self.instance_variable_set(iv_symbol, opts[symbol]) end + + opts[symbol] end private - # Return the value of a parameter, or nil if it doesn't exist. - def _pv_opts_lookup(opts, key) - if opts.has_key?(key.to_s) - opts[key.to_s] - elsif opts.has_key?(key.to_sym) - opts[key.to_sym] - else - nil - end + def explicitly_allows_nil?(key, validation) + validation.has_key?(:is) && _pv_is({ key => nil }, key, validation[:is], raise_error: false) + end + + # Return the value of a parameter, or nil if it doesn't exist. + def _pv_opts_lookup(opts, key) + if opts.has_key?(key.to_s) + opts[key.to_s] + elsif opts.has_key?(key.to_sym) + opts[key.to_sym] + else + nil + end + end + + # Raise an exception if the parameter is not found. + def _pv_required(opts, key, is_required=true, explicitly_allows_nil=false) + if is_required + return true if opts.has_key?(key.to_s) && (explicitly_allows_nil || !opts[key.to_s].nil?) + return true if opts.has_key?(key.to_sym) && (explicitly_allows_nil || !opts[key.to_sym].nil?) + raise Exceptions::ValidationFailed, "Required argument #{key} is missing!" end + end - # Raise an exception if the parameter is not found. - def _pv_required(opts, key, is_required=true) - if is_required - if (opts.has_key?(key.to_s) && !opts[key.to_s].nil?) || - (opts.has_key?(key.to_sym) && !opts[key.to_sym].nil?) - true - else - raise Exceptions::ValidationFailed, "Required argument #{key} is missing!" - end + # + # List of things values must be equal to. + # + # Uses Ruby's `==` to evaluate (equal_to == value). At least one must + # match for the value to be valid. + # + # `nil` passes this validation automatically. + # + # @return [Array,nil] List of things values must be equal to, or nil if + # equal_to is unspecified. + # + def _pv_equal_to(opts, key, to_be) + value = _pv_opts_lookup(opts, key) + unless value.nil? + to_be = Array(to_be) + to_be.each do |tb| + return true if value == tb end + raise Exceptions::ValidationFailed, "Option #{key} must be equal to one of: #{to_be.join(", ")}! You passed #{value.inspect}." end + end - def _pv_equal_to(opts, key, to_be) - value = _pv_opts_lookup(opts, key) - unless value.nil? - passes = false - Array(to_be).each do |tb| - passes = true if value == tb - end - unless passes - raise Exceptions::ValidationFailed, "Option #{key} must be equal to one of: #{to_be.join(", ")}! You passed #{value.inspect}." - end + # + # List of things values must be instances of. + # + # Uses value.kind_of?(kind_of) to evaluate. At least one must match for + # the value to be valid. + # + # `nil` automatically passes this validation. + # + def _pv_kind_of(opts, key, to_be) + value = _pv_opts_lookup(opts, key) + unless value.nil? + to_be = Array(to_be) + to_be.each do |tb| + return true if value.kind_of?(tb) end + raise Exceptions::ValidationFailed, "Option #{key} must be a kind of #{to_be}! You passed #{value.inspect}." end + end - # Raise an exception if the parameter is not a kind_of?(to_be) - def _pv_kind_of(opts, key, to_be) - value = _pv_opts_lookup(opts, key) - unless value.nil? - passes = false - Array(to_be).each do |tb| - passes = true if value.kind_of?(tb) - end - unless passes - raise Exceptions::ValidationFailed, "Option #{key} must be a kind of #{to_be}! You passed #{value.inspect}." + # + # List of method names values must respond to. + # + # Uses value.respond_to?(respond_to) to evaluate. At least one must match + # for the value to be valid. + # + def _pv_respond_to(opts, key, method_name_list) + value = _pv_opts_lookup(opts, key) + unless value.nil? + Array(method_name_list).each do |method_name| + unless value.respond_to?(method_name) + raise Exceptions::ValidationFailed, "Option #{key} must have a #{method_name} method!" end end end + end - # Raise an exception if the parameter does not respond to a given set of methods. - def _pv_respond_to(opts, key, method_name_list) - value = _pv_opts_lookup(opts, key) - unless value.nil? - Array(method_name_list).each do |method_name| - unless value.respond_to?(method_name) - raise Exceptions::ValidationFailed, "Option #{key} must have a #{method_name} method!" + # + # List of things that must not be true about the value. + # + # Calls `value.<thing>?` All responses must be false for the value to be + # valid. + # Values which do not respond to <thing>? are considered valid (because if + # a value doesn't respond to `:readable?`, then it probably isn't + # readable.) + # + # @example + # ```ruby + # property :x, cannot_be: [ :nil, :empty ] + # x [ 1, 2 ] #=> valid + # x 1 #=> valid + # x [] #=> invalid + # x nil #=> invalid + # ``` + # + def _pv_cannot_be(opts, key, predicate_method_base_name) + value = _pv_opts_lookup(opts, key) + if !value.nil? + Array(predicate_method_base_name).each do |method_name| + predicate_method = :"#{method_name}?" + + if value.respond_to?(predicate_method) + if value.send(predicate_method) + raise Exceptions::ValidationFailed, "Option #{key} cannot be #{predicate_method_base_name}" end end end end + end - # Assert that parameter returns false when passed a predicate method. - # For example, :cannot_be => :blank will raise a Exceptions::ValidationFailed - # error value.blank? returns a 'truthy' (not nil or false) value. - # - # Note, this will *PASS* if the object doesn't respond to the method. - # So, to make sure a value is not nil and not blank, you need to do - # both :cannot_be => :blank *and* :cannot_be => :nil (or :required => true) - def _pv_cannot_be(opts, key, predicate_method_base_name) - value = _pv_opts_lookup(opts, key) - predicate_method = (predicate_method_base_name.to_s + "?").to_sym - - if value.respond_to?(predicate_method) - if value.send(predicate_method) - raise Exceptions::ValidationFailed, "Option #{key} cannot be #{predicate_method_base_name}" - end - end + # + # The default value for a property. + # + # When the property is not assigned, this will be used. + # + # If this is a lazy value, it will either be passed the resource as a value, + # or if the lazy proc does not take parameters, it will be run in the + # context of the instance with instance_eval. + # + # @example + # ```ruby + # property :x, default: 10 + # ``` + # + # @example + # ```ruby + # property :x + # property :y, default: lazy { x+2 } + # ``` + # + # @example + # ```ruby + # property :x + # property :y, default: lazy { |r| r.x+2 } + # ``` + # + def _pv_default(opts, key, default_value) + value = _pv_opts_lookup(opts, key) + if value.nil? + default_value = default_value.freeze if !default_value.is_a?(DelayedEvaluator) + opts[key] = default_value end + end - # Assign a default value to a parameter. - def _pv_default(opts, key, default_value) - value = _pv_opts_lookup(opts, key) - if value == nil - opts[key] = default_value + # + # List of regexes values that must match. + # + # Uses regex.match() to evaluate. At least one must match for the value to + # be valid. + # + # `nil` passes regex validation automatically. + # + # @example + # ```ruby + # property :x, regex: [ /abc/, /xyz/ ] + # ``` + # + def _pv_regex(opts, key, regex) + value = _pv_opts_lookup(opts, key) + if !value.nil? + Array(regex).each do |r| + return true if r.match(value.to_s) end + raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} does not match regular expression #{regex.inspect}" end + end - # Check a parameter against a regular expression. - def _pv_regex(opts, key, regex) - value = _pv_opts_lookup(opts, key) - if value != nil - passes = false - [ regex ].flatten.each do |r| - if value != nil - if r.match(value.to_s) - passes = true - end - end - end - unless passes - raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} does not match regular expression #{regex.inspect}" + # + # List of procs we pass the value to. + # + # All procs must return true for the value to be valid. If any procs do + # not return true, the key will be used for the message: `"Property x's + # value :y <message>"`. + # + # @example + # ```ruby + # property :x, callbacks: { "is bigger than 10" => proc { |v| v <= 10 }, "is not awesome" => proc { |v| !v.awesome }} + # ``` + # + def _pv_callbacks(opts, key, callbacks) + raise ArgumentError, "Callback list must be a hash!" unless callbacks.kind_of?(Hash) + value = _pv_opts_lookup(opts, key) + if !value.nil? + callbacks.each do |message, zeproc| + if zeproc.call(value) != true + raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} #{message}!" end end end + end - # Check a parameter against a hash of proc's. - def _pv_callbacks(opts, key, callbacks) - raise ArgumentError, "Callback list must be a hash!" unless callbacks.kind_of?(Hash) - value = _pv_opts_lookup(opts, key) - if value != nil - callbacks.each do |message, zeproc| - if zeproc.call(value) != true - raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} #{message}!" - end - end + # + # Allows a parameter to default to the value of the resource name. + # + # @example + # ```ruby + # property :x, name_property: true + # ``` + # + def _pv_name_property(opts, key, is_name_property=true) + if is_name_property + if opts[key].nil? + opts[key] = self.instance_variable_get("@name") end end + end + alias :_pv_name_attribute :_pv_name_property - # Allow a parameter to default to @name - def _pv_name_attribute(opts, key, is_name_attribute=true) - if is_name_attribute - if opts[key] == nil - opts[key] = self.instance_variable_get("@name") - end + # + # List of valid things values can be. + # + # Uses Ruby's `===` to evaluate (is === value). At least one must match + # for the value to be valid. + # + # If a proc is passed, it is instance_eval'd in the resource, passed the + # value, and must return a truthy or falsey value. + # + # @example Class + # ```ruby + # property :x, String + # x 'valid' #=> valid + # x 1 #=> invalid + # x nil #=> invalid + # + # @example Value + # ```ruby + # property :x, [ :a, :b, :c, nil ] + # x :a #=> valid + # x nil #=> valid + # ``` + # + # @example Regex + # ```ruby + # property :x, /bar/ + # x 'foobar' #=> valid + # x 'foo' #=> invalid + # x nil #=> invalid + # ``` + # + # @example Proc + # ```ruby + # property :x, proc { |x| x > y } + # property :y, default: 2 + # x 3 #=> valid + # x 1 #=> invalid + # ``` + # + # @example PropertyType + # ```ruby + # type = PropertyType.new(is: String) + # property :x, type + # x 'foo' #=> valid + # x 1 #=> invalid + # x nil #=> invalid + # ``` + # + # @example RSpec Matcher + # ```ruby + # include RSpec::Matchers + # property :x, a_string_matching /bar/ + # x 'foobar' #=> valid + # x 'foo' #=> invalid + # x nil #=> invalid + # ``` + # + def _pv_is(opts, key, to_be, raise_error: true) + return true if !opts.has_key?(key.to_s) && !opts.has_key?(key.to_sym) + value = _pv_opts_lookup(opts, key) + to_be = [ to_be ].flatten(1) + to_be.each do |tb| + if tb.is_a?(Proc) + return true if instance_exec(value, &tb) + else + return true if tb === value end end + + if raise_error + raise Exceptions::ValidationFailed, "Option #{key} must be one of: #{to_be.join(", ")}! You passed #{value.inspect}." + else + false + end + end + + # + # Method to mess with a value before it is validated and stored. + # + # Allows you to transform values into a canonical form that is easy to + # work with. + # + # This is passed the value to transform, and is run in the context of the + # instance (so it has access to other resource properties). It must return + # the value that will be stored in the instance. + # + # @example + # ```ruby + # property :x, Integer, coerce: { |v| v.to_i } + # ``` + # + def _pv_coerce(opts, key, coercer) + if opts.has_key?(key.to_s) + opts[key.to_s] = instance_exec(opts[key], &coercer) + elsif opts.has_key?(key.to_sym) + opts[key.to_sym] = instance_exec(opts[key], &coercer) + end + end end end end - diff --git a/lib/chef/platform/priority_map.rb b/lib/chef/platform/priority_map.rb index 3608975b51..d559eece78 100644 --- a/lib/chef/platform/priority_map.rb +++ b/lib/chef/platform/priority_map.rb @@ -3,6 +3,10 @@ require 'chef/node_map' class Chef class Platform class PriorityMap < Chef::NodeMap + def priority(resource_name, priority_array, *filter) + set_priority_array(resource_name.to_sym, priority_array, *filter) + end + # @api private def get_priority_array(node, key) get(node, key) diff --git a/lib/chef/platform/provider_mapping.rb b/lib/chef/platform/provider_mapping.rb index af17d8e1b4..4278b8d24f 100644 --- a/lib/chef/platform/provider_mapping.rb +++ b/lib/chef/platform/provider_mapping.rb @@ -197,7 +197,8 @@ class Chef def resource_matching_provider(platform, version, resource_type) if resource_type.kind_of?(Chef::Resource) - class_name = resource_type.class.to_s.split('::').last + class_name = resource_type.class.name ? resource_type.class.name.split('::').last : + convert_to_class_name(resource_type.resource_name.to_s) begin result = Chef::Provider.const_get(class_name) diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb index 131b72cd23..280277d947 100644 --- a/lib/chef/provider.rb +++ b/lib/chef/provider.rb @@ -26,6 +26,7 @@ require 'chef/mixin/powershell_out' require 'chef/mixin/provides' require 'chef/platform/service_helpers' require 'chef/node_map' +require 'forwardable' class Chef class Provider @@ -65,6 +66,7 @@ class Chef @recipe_name = nil @cookbook_name = nil + self.class.include_resource_dsl_module(new_resource) end def whyrun_mode? @@ -119,11 +121,11 @@ class Chef check_resource_semantics! # user-defined LWRPs may include unsafe load_current_resource methods that cannot be run in whyrun mode - if !whyrun_mode? || whyrun_supported? + if whyrun_mode? && !whyrun_supported? + events.resource_current_state_load_bypassed(@new_resource, @action, @current_resource) + else load_current_resource events.resource_current_state_loaded(@new_resource, @action, @current_resource) - elsif whyrun_mode? && !whyrun_supported? - events.resource_current_state_load_bypassed(@new_resource, @action, @current_resource) end define_resource_requirements @@ -136,9 +138,7 @@ class Chef # we can't execute the action. # in non-whyrun mode, this will still cause the action to be # executed normally. - if whyrun_supported? && !requirements.action_blocked?(@action) - send("action_#{@action}") - elsif whyrun_mode? + if whyrun_mode? && (!whyrun_supported? || requirements.action_blocked?(@action)) events.resource_bypassed(@new_resource, @action, self) else send("action_#{@action}") @@ -183,6 +183,121 @@ class Chef Chef::ProviderResolver.new(node, resource, :nothing).provided_by?(self) end + # + # Include attributes, public and protected methods from this Resource in + # the provider. + # + # If this is set to true, delegate methods are included in the provider so + # that you can call (for example) `attrname` and it will call + # `new_resource.attrname`. + # + # The actual include does not happen until the first time the Provider + # is instantiated (so that we don't have to worry about load order issues). + # + # @param include_resource_dsl [Boolean] Whether to include resource DSL or + # not (defaults to `false`). + # + def self.include_resource_dsl(include_resource_dsl) + @include_resource_dsl = include_resource_dsl + end + + # Create the resource DSL module that forwards resource methods to new_resource + # + # @api private + def self.include_resource_dsl_module(resource) + if @include_resource_dsl && !defined?(@included_resource_dsl_module) + provider_class = self + @included_resource_dsl_module = Module.new do + extend Forwardable + define_singleton_method(:to_s) { "#{resource_class} forwarder module" } + define_singleton_method(:inspect) { to_s } + dsl_methods = + resource.class.public_instance_methods + + resource.class.protected_instance_methods - + provider_class.instance_methods + def_delegators(:new_resource, *dsl_methods) + end + include @included_resource_dsl_module + end + end + + # Enables inline evaluation of resources in provider actions. + # + # Without this option, any resources declared inside the Provider are added + # to the resource collection after the current position at the time the + # action is executed. Because they are added to the primary resource + # collection for the chef run, they can notify other resources outside + # the Provider, and potentially be notified by resources outside the Provider + # (but this is complicated by the fact that they don't exist until the + # provider executes). In this mode, it is impossible to correctly set the + # updated_by_last_action flag on the parent Provider resource, since it + # executes and returns before its component resources are run. + # + # With this option enabled, each action creates a temporary run_context + # with its own resource collection, evaluates the action's code in that + # context, and then converges the resources created. If any resources + # were updated, then this provider's new_resource will be marked updated. + # + # In this mode, resources created within the Provider cannot interact with + # external resources via notifies, though notifications to other + # resources within the Provider will work. Delayed notifications are executed + # at the conclusion of the provider's action, *not* at the end of the + # main chef run. + # + # This mode of evaluation is experimental, but is believed to be a better + # set of tradeoffs than the append-after mode, so it will likely become + # the default in a future major release of Chef. + # + def self.use_inline_resources + extend InlineResources::ClassMethods + include InlineResources + end + + # Chef::Provider::InlineResources + # Implementation of inline resource convergence for providers. See + # Provider.use_inline_resources for a longer explanation. + # + # This code is restricted to a module so that it can be selectively + # applied to providers on an opt-in basis. + # + # @api private + module InlineResources + + # Our run context is a child of the main run context; that gives us a + # whole new resource collection and notification set. + def initialize(resource, run_context) + super(resource, run_context.create_child) + end + + # Class methods for InlineResources. Overrides the `action` DSL method + # with one that enables inline resource convergence. + # + # @api private + module ClassMethods + # Defines an action method on the provider, running the block to + # compile the resources, converging them, and then checking if any + # were updated (and updating new-resource if so) + def action(name, &block) + class_eval <<-EOM, __FILE__, __LINE__+1 + def action_#{name} + return_value = compile_action_#{name} + Chef::Runner.new(run_context).converge + return_value + ensure + if run_context.resource_collection.any? {|r| r.updated? } + new_resource.updated_by_last_action(true) + end + end + EOM + # We put the action in its own method so that super() works. + define_method("compile_action_#{name}", &block) + end + end + + require 'chef/dsl/recipe' + include Chef::DSL::Recipe::FullDSL + end + protected def converge_actions @@ -200,12 +315,14 @@ class Chef # manipulating notifies. converge_by ("evaluate block and run any associated actions") do - saved_run_context = @run_context - @run_context = @run_context.dup - @run_context.resource_collection = Chef::ResourceCollection.new - instance_eval(&block) - Chef::Runner.new(@run_context).converge - @run_context = saved_run_context + saved_run_context = run_context + begin + @run_context = run_context.create_child + instance_eval(&block) + Chef::Runner.new(run_context).converge + ensure + @run_context = saved_run_context + end end end diff --git a/lib/chef/provider/deploy.rb b/lib/chef/provider/deploy.rb index 19e7c01ab1..6d9b7f4397 100644 --- a/lib/chef/provider/deploy.rb +++ b/lib/chef/provider/deploy.rb @@ -373,11 +373,9 @@ class Chef end def gem_resource_collection_runner - gems_collection = Chef::ResourceCollection.new - gem_packages.each { |rbgem| gems_collection.insert(rbgem) } - gems_run_context = run_context.dup - gems_run_context.resource_collection = gems_collection - Chef::Runner.new(gems_run_context) + child_context = run_context.create_child + gem_packages.each { |rbgem| child_context.resource_collection.insert(rbgem) } + Chef::Runner.new(child_context) end def gem_packages diff --git a/lib/chef/provider/lwrp_base.rb b/lib/chef/provider/lwrp_base.rb index b5efbb284d..a96c382a01 100644 --- a/lib/chef/provider/lwrp_base.rb +++ b/lib/chef/provider/lwrp_base.rb @@ -28,52 +28,10 @@ class Chef # Base class from which LWRP providers inherit. class LWRPBase < Provider - # Chef::Provider::LWRPBase::InlineResources - # Implementation of inline resource convergence for LWRP providers. See - # Provider::LWRPBase.use_inline_resources for a longer explanation. - # - # This code is restricted to a module so that it can be selectively - # applied to providers on an opt-in basis. - module InlineResources - - # Class methods for InlineResources. Overrides the `action` DSL method - # with one that enables inline resource convergence. - module ClassMethods - # Defines an action method on the provider, using - # recipe_eval_with_update_check to execute the given block. - def action(name, &block) - define_method("action_#{name}") do - recipe_eval_with_update_check(&block) - end - end - end - - # Executes the given block in a temporary run_context with its own - # resource collection. After the block is executed, any resources - # declared inside are converged, and if any are updated, the - # new_resource will be marked updated. - def recipe_eval_with_update_check(&block) - saved_run_context = @run_context - temp_run_context = @run_context.dup - @run_context = temp_run_context - @run_context.resource_collection = Chef::ResourceCollection.new - - return_value = instance_eval(&block) - Chef::Runner.new(@run_context).converge - return_value - ensure - @run_context = saved_run_context - if temp_run_context.resource_collection.any? {|r| r.updated? } - new_resource.updated_by_last_action(true) - end - end - - end - include Chef::DSL::Recipe # These were previously provided by Chef::Mixin::RecipeDefinitionDSLCore. - # They are not included by its replacment, Chef::DSL::Recipe, but + # They are not included by its replacement, Chef::DSL::Recipe, but # they may be used in existing LWRPs. include Chef::DSL::PlatformIntrospection include Chef::DSL::DataQuery @@ -122,38 +80,6 @@ class Chef provider_class end - # Enables inline evaluation of resources in provider actions. - # - # Without this option, any resources declared inside the LWRP are added - # to the resource collection after the current position at the time the - # action is executed. Because they are added to the primary resource - # collection for the chef run, they can notify other resources outside - # the LWRP, and potentially be notified by resources outside the LWRP - # (but this is complicated by the fact that they don't exist until the - # provider executes). In this mode, it is impossible to correctly set the - # updated_by_last_action flag on the parent LWRP resource, since it - # executes and returns before its component resources are run. - # - # With this option enabled, each action creates a temporary run_context - # with its own resource collection, evaluates the action's code in that - # context, and then converges the resources created. If any resources - # were updated, then this provider's new_resource will be marked updated. - # - # In this mode, resources created within the LWRP cannot interact with - # external resources via notifies, though notifications to other - # resources within the LWRP will work. Delayed notifications are executed - # at the conclusion of the provider's action, *not* at the end of the - # main chef run. - # - # This mode of evaluation is experimental, but is believed to be a better - # set of tradeoffs than the append-after mode, so it will likely become - # the default in a future major release of Chef. - # - def use_inline_resources - extend InlineResources::ClassMethods - include InlineResources - end - # DSL for defining a provider's actions. def action(name, &block) define_method("action_#{name}") do diff --git a/lib/chef/recipe.rb b/lib/chef/recipe.rb index b4d37c2d61..262560f754 100644 --- a/lib/chef/recipe.rb +++ b/lib/chef/recipe.rb @@ -36,14 +36,7 @@ class Chef # A Recipe object is the context in which Chef recipes are evaluated. class Recipe - include Chef::DSL::DataQuery - include Chef::DSL::PlatformIntrospection - include Chef::DSL::IncludeRecipe - include Chef::DSL::Recipe - include Chef::DSL::RegistryHelper - include Chef::DSL::RebootPending - include Chef::DSL::Audit - include Chef::DSL::Powershell + include Chef::DSL::Recipe::FullDSL include Chef::Mixin::FromFile include Chef::Mixin::Deprecation diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index 418b0dc1ce..8d2532dac4 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -58,8 +58,6 @@ class Chef include Chef::Mixin::ShellOut include Chef::Mixin::PowershellOut - NULL_ARG = Object.new - # # The node the current Chef run is using. # @@ -187,8 +185,7 @@ class Chef end self.action = arg else - # Pull the action from the class if it's not set - @action || self.class.default_action + @action end end @@ -531,9 +528,7 @@ class Chef # # Equivalent to #ignore_failure. # - def epic_fail(arg=nil) - ignore_failure(arg) - end + alias :epic_fail :ignore_failure # # Make this resource into an exact (shallow) copy of the other resource. @@ -688,26 +683,133 @@ class Chef # # The provider class for this resource. # + # If `action :x do ... end` has been declared on this resource or its + # superclasses, this will return the `action_provider_class`. + # # If this is not set, `provider_for_action` will dynamically determine the # provider. # # @param arg [String, Symbol, Class] Sets the provider class for this resource. # If passed a String or Symbol, e.g. `:file` or `"file"`, looks up the # provider based on the name. + # # @return The provider class for this resource. # + # @see Chef::Resource.action_provider_class + # def provider(arg=nil) klass = if arg.kind_of?(String) || arg.kind_of?(Symbol) lookup_provider_constant(arg) else arg end - set_or_return(:provider, klass, kind_of: [ Class ]) + set_or_return(:provider, klass, kind_of: [ Class ]) || + self.class.action_provider_class end def provider=(arg) provider(arg) end + # + # Create a property on this resource class. + # + # If a superclass has this property, or if this property has already been + # defined by this resource, this will *override* the previous value. + # + # @param name [Symbol] The name of the property. + # @param type [Object,Array<Object>] The type(s) of this property. + # If present, this is prepended to the `is` validation option. + # @param options [Hash<Symbol,Object>] Validation options. + # @option options [Object,Array] :is An object, or list of + # objects, that must match the value using Ruby's `===` operator + # (`options[:is].any? { |v| v === value }`). + # @option options [Object,Array] :equal_to An object, or list + # of objects, that must be equal to the value using Ruby's `==` + # operator (`options[:is].any? { |v| v == value }`) + # @option options [Regexp,Array<Regexp>] :regex An object, or + # list of objects, that must match the value with `regex.match(value)`. + # @option options [Class,Array<Class>] :kind_of A class, or + # list of classes, that the value must be an instance of. + # @option options [Hash<String,Proc>] :callbacks A hash of + # messages -> procs, all of which match the value. The proc must + # return a truthy or falsey value (true means it matches). + # @option options [Symbol,Array<Symbol>] :respond_to A method + # name, or list of method names, the value must respond to. + # @option options [Symbol,Array<Symbol>] :cannot_be A property, + # or a list of properties, that the value cannot have (such as `:nil` or + # `:empty`). The method with a questionmark at the end is called on the + # value (e.g. `value.empty?`). If the value does not have this method, + # it is considered valid (i.e. if you don't respond to `empty?` we + # assume you are not empty). + # @option options [Proc] :coerce A proc which will be called to + # transform the user input to canonical form. The value is passed in, + # and the transformed value returned as output. Lazy values will *not* + # be passed to this method until after they are evaluated. Called in the + # context of the resource (meaning you can access other properties). + # @option options [Boolean] :required `true` if this property + # must be present; `false` otherwise. This is checked after the resource + # is fully initialized. + # @option options [Boolean] :name_property `true` if this + # property defaults to the same value as `name`. Equivalent to + # `default: lazy { name }`, except that #property_is_set? will + # return `true` if the property is set *or* if `name` is set. + # @option options [Boolean] :name_attribute Same as `name_property`. + # @option options [Object] :default The value this property + # will return if the user does not set one. If this is `lazy`, it will + # be run in the context of the instance (and able to access other + # properties). + # + # @example Bare property + # property :x + # + # @example With just a type + # property :x, String + # + # @example With just options + # property :x, default: 'hi' + # + # @example With type and options + # property :x, String, default: 'hi' + # + def self.property(name, type=NOT_PASSED, **options) + name = name.to_sym + + if type != NOT_PASSED + if options[:is] + options[:is] = ([ type ] + [ options[:is] ]).flatten(1) + else + options[:is] = type + end + end + + define_method(name) do |value=NOT_PASSED| + set_or_return(name, value, options) + end + define_method("#{name}=") do |value| + set_or_return(name, value, options) + end + end + + # + # Whether this property has been set (or whether it has a default that has + # been retrieved). + # + def property_is_set?(name) + name = name.to_sym + instance_variable_defined?("@#{name}") + end + + # + # Create a lazy value for assignment to a default value. + # + # @param block The block to run when the value is retrieved. + # + # @return [Chef::DelayedEvaluator] The lazy value + # + def self.lazy(&block) + DelayedEvaluator.new(&block) + end + # Set or return the list of "state attributes" implemented by the Resource # subclass. State attributes are attributes that describe the desired state # of the system, such as file permissions or ownership. In general, state @@ -773,8 +875,8 @@ class Chef # have. # attr_accessor :allowed_actions - def allowed_actions(value=NULL_ARG) - if value != NULL_ARG + def allowed_actions(value=NOT_PASSED) + if value != NOT_PASSED self.allowed_actions = value end @allowed_actions @@ -908,9 +1010,9 @@ class Chef # # @return [Symbol] The name of this resource type (e.g. `:execute`). # - def self.resource_name(name=NULL_ARG) + def self.resource_name(name=NOT_PASSED) # Setter - if name != NULL_ARG + if name != NOT_PASSED remove_canonical_dsl # Set the resource_name and call provides @@ -925,7 +1027,6 @@ class Chef @resource_name = nil end end - @resource_name end def self.resource_name=(name) @@ -933,6 +1034,19 @@ class Chef end # + # Use the class name as the resource name. + # + # Munges the last part of the class name from camel case to snake case, + # and sets the resource_name to that: + # + # A::B::BlahDBlah -> blah_d_blah + # + def self.use_automatic_resource_name + automatic_name = convert_to_snake_case(self.name.split('::')[-1]) + resource_name automatic_name + end + + # # The module where Chef should look for providers for this resource. # The provider for `MyResource` will be looked up using # `provider_base::MyResource`. Defaults to `Chef::Provider`. @@ -988,8 +1102,8 @@ class Chef # # @return [Symbol,Array<Symbol>] The default actions for the resource. # - def self.default_action(action_name=NULL_ARG) - unless action_name.equal?(NULL_ARG) + def self.default_action(action_name=NOT_PASSED) + unless action_name.equal?(NOT_PASSED) if action_name.is_a?(Array) @default_action = action_name.map { |arg| arg.to_sym } else @@ -1008,6 +1122,91 @@ class Chef end end def self.default_action=(action_name) + default_action action_name + end + + # + # Define an action on this resource. + # + # The action is defined as a *recipe* block that will be compiled and then + # converged when the action is taken (when Resource is converged). The recipe + # has access to the resource's attributes and methods, as well as the Chef + # recipe DSL. + # + # Resources in the action recipe may notify and subscribe to other resources + # within the action recipe, but cannot notify or subscribe to resources + # in the main Chef run. + # + # Resource actions are *inheritable*: if resource A defines `action :create` + # and B is a subclass of A, B gets all of A's actions. Additionally, + # resource B can define `action :create` and call `super()` to invoke A's + # action code. + # + # The first action defined (besides `:nothing`) will become the default + # action for the resource. + # + # @param name [Symbol] The action name to define. + # @param recipe_block The recipe to run when the action is taken. This block + # takes no parameters, and will be evaluated in a new context containing: + # + # - The resource's public and protected methods (including attributes) + # - The Chef Recipe DSL (file, etc.) + # - super() referring to the parent version of the action (if any) + # + # @return The Action class implementing the action + # + def self.action(action, &recipe_block) + action = action.to_sym + new_action_provider_class.action(action, &recipe_block) + self.allowed_actions += [ action ] + default_action action if default_action == :nothing + end + + # + # The action provider class is an automatic `Provider` created to handle + # actions declared by `action :x do ... end`. + # + # This class will be returned by `resource.provider` if `resource.provider` + # is not set. `provider_for_action` will also use this instead of calling + # out to `Chef::ProviderResolver`. + # + # If the user has not declared actions on this class or its superclasses + # using `action :x do ... end`, then there is no need for this class and + # `action_provider_class` will be `nil`. + # + # @api private + # + def self.action_provider_class + @action_provider_class || + # If the superclass needed one, then we need one as well. + if superclass.respond_to?(:action_provider_class) && superclass.action_provider_class + new_action_provider_class + end + end + + # + # Ensure the action provider class actually gets created. This is called + # when the user does `action :x do ... end`. + # + # @api private + def self.new_action_provider_class + return @action_provider_class if @action_provider_class + + if superclass.respond_to?(:action_provider_class) + base_provider = superclass.action_provider_class + end + base_provider ||= Chef::Provider + + resource_class = self + @action_provider_class = Class.new(base_provider) do + use_inline_resources + include_resource_dsl true + define_singleton_method(:to_s) { "#{resource_class} action provider" } + define_singleton_method(:inspect) { to_s } + define_method(:load_current_resource) {} + end + end + def self.default_action=(action_name) default_action(action_name) end @@ -1082,7 +1281,7 @@ class Chef class << self # back-compat - # NOTE: that we do not support unregistering classes as descendents like + # NOTE: that we do not support unregistering classes as descendants like # we used to for LWRP unloading because that was horrible and removed in # Chef-12. # @deprecated @@ -1208,7 +1407,8 @@ class Chef end def provider_for_action(action) - provider = Chef::ProviderResolver.new(node, self, action).resolve.new(self, run_context) + provider_class = Chef::ProviderResolver.new(node, self, action).resolve + provider = provider_class.new(self, run_context) provider.action = action provider end @@ -1318,23 +1518,22 @@ class Chef # when Chef::Resource::MyLwrp # end # - resource_subclass = class_eval <<-EOM, __FILE__, __LINE__+1 - class Chef::Resource::#{class_name} < resource_class - resource_name nil # we do not actually provide anything - def initialize(*args, &block) - Chef::Log.deprecation("Using an LWRP by its name (#{class_name}) directly is no longer supported in Chef 13 and will be removed. Use Chef::Resource.resource_for_node(node, name) instead.") + resource_subclass = Class.new(resource_class) do + resource_name nil # we do not actually provide anything + def initialize(*args, &block) + Chef::Log.deprecation("Using an LWRP by its name (#{self.class.name}) directly is no longer supported in Chef 13 and will be removed. Use Chef::Resource.resource_for_node(node, name) instead.") + super + end + def self.resource_name(*args) + if args.empty? + @resource_name ||= superclass.resource_name + else super end - def self.resource_name(*args) - if args.empty? - @resource_name ||= superclass.resource_name - else - super - end - end - self end - EOM + self + end + eval("Chef::Resource::#{class_name} = resource_subclass") # Make case, is_a and kind_of work with the new subclass, for backcompat. # Any subclass of Chef::Resource::ResourceClass is already a subclass of resource_class # Any subclass of resource_class is considered a subclass of Chef::Resource::ResourceClass diff --git a/lib/chef/resource/lwrp_base.rb b/lib/chef/resource/lwrp_base.rb index ef3c2b5bba..443e0ed819 100644 --- a/lib/chef/resource/lwrp_base.rb +++ b/lib/chef/resource/lwrp_base.rb @@ -74,13 +74,7 @@ class Chef resource_class end - # Define an attribute on this resource, including optional validation - # parameters. - def attribute(attr_name, validation_opts={}) - define_method(attr_name) do |arg=nil| - set_or_return(attr_name.to_sym, arg, validation_opts) - end - end + alias :attribute :property # Adds +action_names+ to the list of valid actions for this resource. # Does not include superclass's action list when appending. diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb index 44b05f0cc0..f55d1740b1 100644 --- a/lib/chef/run_context.rb +++ b/lib/chef/run_context.rb @@ -25,118 +25,223 @@ require 'chef/log' require 'chef/recipe' require 'chef/run_context/cookbook_compiler' require 'chef/event_dispatch/events_output_stream' +require 'forwardable' class Chef # == Chef::RunContext # Value object that loads and tracks the context of a Chef run class RunContext + # + # Global state + # - # Chef::Node object for this run + # + # The node for this run + # + # @return [Chef::Node] + # attr_reader :node - # Chef::CookbookCollection for this run + # + # The set of cookbooks involved in this run + # + # @return [Chef::CookbookCollection] + # attr_reader :cookbook_collection + # # Resource Definitions for this run. Populated when the files in # +definitions/+ are evaluated (this is triggered by #load). + # + # @return [Array[Chef::ResourceDefinition]] + # attr_reader :definitions - ### - # These need to be settable so deploy can run a resource_collection - # independent of any cookbooks via +recipe_eval+ + # + # Event dispatcher for this run. + # + # @return [Chef::EventDispatch::Dispatcher] + # + attr_reader :events - # The Chef::ResourceCollection for this run. Populated by evaluating - # recipes, which is triggered by #load. (See also: CookbookCompiler) - attr_accessor :resource_collection + # + # Hash of factoids for a reboot request. + # + # @return [Hash] + # + attr_accessor :reboot_info + # + # Scoped state + # + + # + # The parent run context. + # + # @return [Chef::RunContext] The parent run context, or `nil` if this is the + # root context. + # + attr_reader :parent_run_context + + # + # The collection of resources intended to be converged (and able to be + # notified). + # + # @return [Chef::ResourceCollection] + # + # @see CookbookCompiler + # + attr_reader :resource_collection + + # # The list of control groups to execute during the audit phase - attr_accessor :audits + # + attr_reader :audits + # + # Notification handling + # + + # # A Hash containing the immediate notifications triggered by resources # during the converge phase of the chef run. - attr_accessor :immediate_notification_collection + # + # @return [Hash[String, Array[Chef::Resource::Notification]]] A hash from + # <notifying resource name> => <list of notifications it sent> + # + attr_reader :immediate_notification_collection + # # A Hash containing the delayed (end of run) notifications triggered by # resources during the converge phase of the chef run. - attr_accessor :delayed_notification_collection - - # Event dispatcher for this run. - attr_reader :events - - # Hash of factoids for a reboot request. - attr_reader :reboot_info + # + # @return [Hash[String, Array[Chef::Resource::Notification]]] A hash from + # <notifying resource name> => <list of notifications it sent> + # + attr_reader :delayed_notification_collection # Creates a new Chef::RunContext object and populates its fields. This object gets # used by the Chef Server to generate a fully compiled recipe list for a node. # - # === Returns - # object<Chef::RunContext>:: Duh. :) + # @param node [Chef::Node] The node to run against. + # @param cookbook_collection [Chef::CookbookCollection] The cookbooks + # involved in this run. + # @param events [EventDispatch::Dispatcher] The event dispatcher for this + # run. + # def initialize(node, cookbook_collection, events) @node = node @cookbook_collection = cookbook_collection - @resource_collection = Chef::ResourceCollection.new - @audits = {} - @immediate_notification_collection = Hash.new {|h,k| h[k] = []} - @delayed_notification_collection = Hash.new {|h,k| h[k] = []} - @definitions = Hash.new - @loaded_recipes = {} - @loaded_attributes = {} @events = events - @reboot_info = {} - @node.run_context = self - @node.set_cookbook_attribute + node.run_context = self + node.set_cookbook_attribute + + @definitions = Hash.new + @loaded_recipes_hash = {} + @loaded_attributes_hash = {} + @reboot_info = {} @cookbook_compiler = nil + + initialize_child_state end - # Triggers the compile phase of the chef run. Implemented by - # Chef::RunContext::CookbookCompiler + # + # Triggers the compile phase of the chef run. + # + # @param run_list_expansion [Chef::RunList::RunListExpansion] The run list. + # @see Chef::RunContext::CookbookCompiler + # def load(run_list_expansion) @cookbook_compiler = CookbookCompiler.new(self, run_list_expansion, events) - @cookbook_compiler.compile + cookbook_compiler.compile end - # Adds an immediate notification to the - # +immediate_notification_collection+. The notification should be a - # Chef::Resource::Notification or duck type. + # + # Initialize state that applies to both Chef::RunContext and Chef::ChildRunContext + # + def initialize_child_state + @audits = {} + @resource_collection = Chef::ResourceCollection.new + @immediate_notification_collection = Hash.new {|h,k| h[k] = []} + @delayed_notification_collection = Hash.new {|h,k| h[k] = []} + end + + # + # Adds an immediate notification to the +immediate_notification_collection+. + # + # @param [Chef::Resource::Notification] The notification to add. + # def notifies_immediately(notification) nr = notification.notifying_resource if nr.instance_of?(Chef::Resource) - @immediate_notification_collection[nr.name] << notification + immediate_notification_collection[nr.name] << notification else - @immediate_notification_collection[nr.declared_key] << notification + immediate_notification_collection[nr.declared_key] << notification end end - # Adds a delayed notification to the +delayed_notification_collection+. The - # notification should be a Chef::Resource::Notification or duck type. + # + # Adds a delayed notification to the +delayed_notification_collection+. + # + # @param [Chef::Resource::Notification] The notification to add. + # def notifies_delayed(notification) nr = notification.notifying_resource if nr.instance_of?(Chef::Resource) - @delayed_notification_collection[nr.name] << notification + delayed_notification_collection[nr.name] << notification else - @delayed_notification_collection[nr.declared_key] << notification + delayed_notification_collection[nr.declared_key] << notification end end + # + # Get the list of immediate notifications sent by the given resource. + # + # TODO seriously, this is actually wrong. resource.name is not unique, + # you need the type as well. + # + # @return [Array[Notification]] + # def immediate_notifications(resource) if resource.instance_of?(Chef::Resource) - return @immediate_notification_collection[resource.name] + return immediate_notification_collection[resource.name] else - return @immediate_notification_collection[resource.declared_key] + return immediate_notification_collection[resource.declared_key] end end + # + # Get the list of delayed (end of run) notifications sent by the given + # resource. + # + # TODO seriously, this is actually wrong. resource.name is not unique, + # you need the type as well. + # + # @return [Array[Notification]] + # def delayed_notifications(resource) if resource.instance_of?(Chef::Resource) - return @delayed_notification_collection[resource.name] + return delayed_notification_collection[resource.name] else - return @delayed_notification_collection[resource.declared_key] + return delayed_notification_collection[resource.declared_key] end end + # + # Cookbook and recipe loading + # + + # # Evaluates the recipes +recipe_names+. Used by DSL::IncludeRecipe + # + # @param recipe_names [Array[String]] The list of recipe names (e.g. + # 'my_cookbook' or 'my_cookbook::my_resource'). + # @param current_cookbook The cookbook we are currently running in. + # + # @see DSL::IncludeRecipe#include_recipe + # def include_recipe(*recipe_names, current_cookbook: nil) result_recipes = Array.new recipe_names.flatten.each do |recipe_name| @@ -147,7 +252,21 @@ class Chef result_recipes end + # # Evaluates the recipe +recipe_name+. Used by DSL::IncludeRecipe + # + # TODO I am sort of confused why we have both this and include_recipe ... + # I don't see anything different beyond accepting and returning an + # array of recipes. + # + # @param recipe_names [Array[String]] The recipe name (e.g 'my_cookbook' or + # 'my_cookbook::my_resource'). + # @param current_cookbook The cookbook we are currently running in. + # + # @return A truthy value if the load occurred; `false` if already loaded. + # + # @see DSL::IncludeRecipe#load_recipe + # def load_recipe(recipe_name, current_cookbook: nil) Chef::Log.debug("Loading Recipe #{recipe_name} via include_recipe") @@ -175,6 +294,15 @@ ERROR_MESSAGE end end + # + # Load the given recipe from a filename. + # + # @param recipe_file [String] The recipe filename. + # + # @return [Chef::Recipe] The loaded recipe. + # + # @raise [Chef::Exceptions::RecipeNotFound] If the file does not exist. + # def load_recipe_file(recipe_file) if !File.exist?(recipe_file) raise Chef::Exceptions::RecipeNotFound, "could not find recipe file #{recipe_file}" @@ -186,8 +314,19 @@ ERROR_MESSAGE recipe end - # Looks up an attribute file given the +cookbook_name+ and - # +attr_file_name+. Used by DSL::IncludeAttribute + # + # Look up an attribute filename. + # + # @param cookbook_name [String] The cookbook name of the attribute file. + # @param attr_file_name [String] The attribute file's name (not path). + # + # @return [String] The filename. + # + # @see DSL::IncludeAttribute#include_attribute + # + # @raise [Chef::Exceptions::CookbookNotFound] If the cookbook could not be found. + # @raise [Chef::Exceptions::AttributeNotFound] If the attribute file could not be found. + # def resolve_attribute(cookbook_name, attr_file_name) cookbook = cookbook_collection[cookbook_name] raise Chef::Exceptions::CookbookNotFound, "could not find cookbook #{cookbook_name} while loading attribute #{name}" unless cookbook @@ -198,76 +337,152 @@ ERROR_MESSAGE attribute_filename end - # An Array of all recipes that have been loaded. This is stored internally - # as a Hash, so ordering is predictable. # - # Recipe names are given in fully qualified form, e.g., the recipe "nginx" - # will be given as "nginx::default" + # A list of all recipes that have been loaded. + # + # This is stored internally as a Hash, so ordering is predictable. + # + # TODO is the above statement true in a 1.9+ ruby world? Is it relevant? + # + # @return [Array[String]] A list of recipes in fully qualified form, e.g. + # the recipe "nginx" will be given as "nginx::default". + # + # @see #loaded_recipe? To determine if a particular recipe has been loaded. # - # To determine if a particular recipe has been loaded, use #loaded_recipe? def loaded_recipes - @loaded_recipes.keys + loaded_recipes_hash.keys end - # An Array of all attributes files that have been loaded. Stored internally - # using a Hash, so order is predictable. # - # Attribute file names are given in fully qualified form, e.g., - # "nginx::default" instead of "nginx". + # A list of all attributes files that have been loaded. + # + # Stored internally using a Hash, so order is predictable. + # + # TODO is the above statement true in a 1.9+ ruby world? Is it relevant? + # + # @return [Array[String]] A list of attribute file names in fully qualified + # form, e.g. the "nginx" will be given as "nginx::default". + # def loaded_attributes - @loaded_attributes.keys + loaded_attributes_hash.keys end + # + # Find out if a given recipe has been loaded. + # + # @param cookbook [String] Cookbook name. + # @param recipe [String] Recipe name. + # + # @return [Boolean] `true` if the recipe has been loaded, `false` otherwise. + # def loaded_fully_qualified_recipe?(cookbook, recipe) - @loaded_recipes.has_key?("#{cookbook}::#{recipe}") + loaded_recipes_hash.has_key?("#{cookbook}::#{recipe}") end - # Returns true if +recipe+ has been loaded, false otherwise. Default recipe - # names are expanded, so `loaded_recipe?("nginx")` and - # `loaded_recipe?("nginx::default")` are valid and give identical results. + # + # Find out if a given recipe has been loaded. + # + # @param recipe [String] Recipe name. "nginx" and "nginx::default" yield + # the same results. + # + # @return [Boolean] `true` if the recipe has been loaded, `false` otherwise. + # def loaded_recipe?(recipe) cookbook, recipe_name = Chef::Recipe.parse_recipe_name(recipe) loaded_fully_qualified_recipe?(cookbook, recipe_name) end + # + # Mark a given recipe as having been loaded. + # + # @param cookbook [String] Cookbook name. + # @param recipe [String] Recipe name. + # + def loaded_recipe(cookbook, recipe) + loaded_recipes_hash["#{cookbook}::#{recipe}"] = true + end + + # + # Find out if a given attribute file has been loaded. + # + # @param cookbook [String] Cookbook name. + # @param attribute_file [String] Attribute file name. + # + # @return [Boolean] `true` if the recipe has been loaded, `false` otherwise. + # def loaded_fully_qualified_attribute?(cookbook, attribute_file) - @loaded_attributes.has_key?("#{cookbook}::#{attribute_file}") + loaded_attributes_hash.has_key?("#{cookbook}::#{attribute_file}") end + # + # Mark a given attribute file as having been loaded. + # + # @param cookbook [String] Cookbook name. + # @param attribute_file [String] Attribute file name. + # def loaded_attribute(cookbook, attribute_file) - @loaded_attributes["#{cookbook}::#{attribute_file}"] = true + loaded_attributes_hash["#{cookbook}::#{attribute_file}"] = true end ## # Cookbook File Introspection + # + # Find out if the cookbook has the given template. + # + # @param cookbook [String] Cookbook name. + # @param template_name [String] Template name. + # + # @return [Boolean] `true` if the template is in the cookbook, `false` + # otherwise. + # @see Chef::CookbookVersion#has_template_for_node? + # def has_template_in_cookbook?(cookbook, template_name) cookbook = cookbook_collection[cookbook] cookbook.has_template_for_node?(node, template_name) end + # + # Find out if the cookbook has the given file. + # + # @param cookbook [String] Cookbook name. + # @param cb_file_name [String] File name. + # + # @return [Boolean] `true` if the file is in the cookbook, `false` + # otherwise. + # @see Chef::CookbookVersion#has_cookbook_file_for_node? + # def has_cookbook_file_in_cookbook?(cookbook, cb_file_name) cookbook = cookbook_collection[cookbook] cookbook.has_cookbook_file_for_node?(node, cb_file_name) end - # Delegates to CookbookCompiler#unreachable_cookbook? - # Used to raise an error when attempting to load a recipe belonging to a - # cookbook that is not in the dependency graph. See also: CHEF-4367 + # + # Find out whether the given cookbook is in the cookbook dependency graph. + # + # @param cookbook_name [String] Cookbook name. + # + # @return [Boolean] `true` if the cookbook is reachable, `false` otherwise. + # + # @see Chef::CookbookCompiler#unreachable_cookbook? def unreachable_cookbook?(cookbook_name) - @cookbook_compiler.unreachable_cookbook?(cookbook_name) + cookbook_compiler.unreachable_cookbook?(cookbook_name) end + # # Open a stream object that can be printed into and will dispatch to events # - # == Arguments - # options is a hash with these possible options: - # - name: a string that identifies the stream to the user. Preferably short. + # @param name [String] The name of the stream. + # @param options [Hash] Other options for the stream. + # + # @return [EventDispatch::EventsOutputStream] The created stream. # - # Pass a block and the stream will be yielded to it, and close on its own - # at the end of the block. - def open_stream(options = {}) - stream = EventDispatch::EventsOutputStream.new(events, options) + # @yield If a block is passed, it will be run and the stream will be closed + # afterwards. + # @yieldparam stream [EventDispatch::EventsOutputStream] The created stream. + # + def open_stream(name: nil, **options) + stream = EventDispatch::EventsOutputStream.new(events, name: name, **options) if block_given? begin yield stream @@ -280,31 +495,130 @@ ERROR_MESSAGE end # there are options for how to handle multiple calls to these functions: - # 1. first call always wins (never change @reboot_info once set). - # 2. last call always wins (happily change @reboot_info whenever). + # 1. first call always wins (never change reboot_info once set). + # 2. last call always wins (happily change reboot_info whenever). # 3. raise an exception on the first conflict. # 4. disable reboot after this run if anyone ever calls :cancel. # 5. raise an exception on any second call. # 6. ? def request_reboot(reboot_info) - Chef::Log::info "Changing reboot status from #{@reboot_info.inspect} to #{reboot_info.inspect}" + Chef::Log::info "Changing reboot status from #{self.reboot_info.inspect} to #{reboot_info.inspect}" @reboot_info = reboot_info end def cancel_reboot - Chef::Log::info "Changing reboot status from #{@reboot_info.inspect} to {}" + Chef::Log::info "Changing reboot status from #{reboot_info.inspect} to {}" @reboot_info = {} end def reboot_requested? - @reboot_info.size > 0 + reboot_info.size > 0 + end + + # + # Create a child RunContext. + # + def create_child + ChildRunContext.new(self) end - private + protected - def loaded_recipe(cookbook, recipe) - @loaded_recipes["#{cookbook}::#{recipe}"] = true + attr_reader :cookbook_compiler + attr_reader :loaded_attributes_hash + attr_reader :loaded_recipes_hash + + module Deprecated + ### + # These need to be settable so deploy can run a resource_collection + # independent of any cookbooks via +recipe_eval+ + + def audits=(value) + Chef::Log.deprecation("Setting run_context.audits will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.") + end + + def immediate_notification_collection=(value) + Chef::Log.deprecation("Setting run_context.immediate_notification_collection will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.") + end + + def delayed_notification_collection=(value) + Chef::Log.deprecation("Setting run_context.delayed_notification_collection will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.") + end end + prepend Deprecated + + + # + # A child run context. Delegates all root context calls to its parent. + # + # @api private + # + class ChildRunContext < RunContext + extend Forwardable + def_delegators :parent_run_context, *%w( + cancel_reboot + config + cookbook_collection + cookbook_compiler + definitions + events + has_cookbook_file_in_cookbook? + has_template_in_cookbook? + load + loaded_attribute + loaded_attributes + loaded_attributes_hash + loaded_fully_qualified_attribute? + loaded_fully_qualified_recipe? + loaded_recipe + loaded_recipe? + loaded_recipes + loaded_recipes_hash + node + open_stream + reboot_info + reboot_info= + reboot_requested? + request_reboot + resolve_attribute + unreachable_cookbook? + ) + + def initialize(parent_run_context) + @parent_run_context = parent_run_context + + # We don't call super, because we don't bother initializing stuff we're + # going to delegate to the parent anyway. Just initialize things that + # every instance needs. + initialize_child_state + end + CHILD_STATE = %w( + audits + audits= + create_child + delayed_notification_collection + delayed_notification_collection= + delayed_notifications + immediate_notification_collection + immediate_notification_collection= + immediate_notifications + include_recipe + initialize_child_state + load_recipe + load_recipe_file + notifies_immediately + notifies_delayed + parent_run_context + resource_collection + resource_collection= + ).map { |x| x.to_sym } + + # Verify that we didn't miss any methods + missing_methods = superclass.instance_methods(false) - instance_methods(false) - CHILD_STATE + if !missing_methods.empty? + raise "ERROR: not all methods of RunContext accounted for in ChildRunContext! All methods must be marked as child methods with CHILD_STATE or delegated to the parent_run_context. Missing #{missing_methods.join(", ")}." + end + end end end diff --git a/lib/chef/version.rb b/lib/chef/version.rb index 80fd422c55..743a99824d 100644 --- a/lib/chef/version.rb +++ b/lib/chef/version.rb @@ -21,7 +21,7 @@ class Chef CHEF_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__))) - VERSION = '12.4.0.rc.2' + VERSION = '12.5.0.current.0' end # diff --git a/spec/data/run_context/cookbooks/include/recipes/default.rb b/spec/data/run_context/cookbooks/include/recipes/default.rb new file mode 100644 index 0000000000..8d22994252 --- /dev/null +++ b/spec/data/run_context/cookbooks/include/recipes/default.rb @@ -0,0 +1,24 @@ +module ::RanResources + def self.resources + @resources ||= [] + end +end +class RunContextCustomResource < Chef::Resource + action :create do + ruby_block '4' do + block { RanResources.resources << 4 } + end + recipe_eval do + ruby_block '1' do + block { RanResources.resources << 1 } + end + include_recipe 'include::includee' + ruby_block '3' do + block { RanResources.resources << 3 } + end + end + ruby_block '5' do + block { RanResources.resources << 5 } + end + end +end diff --git a/spec/data/run_context/cookbooks/include/recipes/includee.rb b/spec/data/run_context/cookbooks/include/recipes/includee.rb new file mode 100644 index 0000000000..87bb7f114e --- /dev/null +++ b/spec/data/run_context/cookbooks/include/recipes/includee.rb @@ -0,0 +1,3 @@ +ruby_block '2' do + block { RanResources.resources << 2 } +end diff --git a/spec/integration/recipes/lwrp_spec.rb b/spec/integration/recipes/lwrp_spec.rb index e93763fddc..7ecdfc7c3a 100644 --- a/spec/integration/recipes/lwrp_spec.rb +++ b/spec/integration/recipes/lwrp_spec.rb @@ -45,12 +45,8 @@ log_level :warn EOM result = shell_out("#{chef_client} -c \"#{path_to('config/client.rb')}\" --no-color -F doc -o 'l-w-r-p::default'", :cwd => chef_dir) - actual = result.stdout.lines.map { |l| l.chomp }.join("\n") - expected = <<EOM - * l_w_r_p_foo[me] action create (up to date) -EOM - expected = expected.lines.map { |l| l.chomp }.join("\n") - expect(actual).to include(expected) + expect(result.stdout).to match(/\* l_w_r_p_foo\[me\] action create \(up to date\)/) + expect(result.stdout).not_to match(/WARN: You are overriding l_w_r_p_foo/) result.error! end end diff --git a/spec/integration/recipes/recipe_dsl_spec.rb b/spec/integration/recipes/recipe_dsl_spec.rb index 6bbb9a5c4c..5426dce080 100644 --- a/spec/integration/recipes/recipe_dsl_spec.rb +++ b/spec/integration/recipes/recipe_dsl_spec.rb @@ -969,4 +969,29 @@ describe "Recipe DSL methods" do end end end + + context "with a dynamically defined resource and regular provider" do + before(:context) do + Class.new(Chef::Resource) do + resource_name :lw_resource_with_hw_provider_test_case + default_action :create + attr_accessor :created_provider + end + class Chef::Provider::LwResourceWithHwProviderTestCase < Chef::Provider + def load_current_resource + end + def action_create + new_resource.created_provider = self.class + end + end + end + + it "looks up the provider in Chef::Provider converting the resource name from snake case to camel case" do + resource = nil + recipe = converge { + resource = lw_resource_with_hw_provider_test_case 'blah' do; end + } + expect(resource.created_provider).to eq(Chef::Provider::LwResourceWithHwProviderTestCase) + end + end end diff --git a/spec/integration/recipes/resource_action_spec.rb b/spec/integration/recipes/resource_action_spec.rb new file mode 100644 index 0000000000..4786294803 --- /dev/null +++ b/spec/integration/recipes/resource_action_spec.rb @@ -0,0 +1,343 @@ +require 'support/shared/integration/integration_helper' + +describe "Resource.action" do + include IntegrationSupport + + def converge(str=nil, file=nil, line=nil, &block) + if block + super(&block) + else + super() do + eval(str, nil, file, line) + end + end + end + + shared_context "ActionJackson" do + it "The default action is the first declared action" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + end + EOM + expect(ActionJackson.ran_action).to eq :access_recipe_dsl + expect(ActionJackson.succeeded).to eq true + end + + it "The action can access recipe DSL" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_recipe_dsl + end + EOM + expect(ActionJackson.ran_action).to eq :access_recipe_dsl + expect(ActionJackson.succeeded).to eq true + end + + it "The action can access attributes" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_attribute + end + EOM + expect(ActionJackson.ran_action).to eq :access_attribute + expect(ActionJackson.succeeded).to eq 'foo!' + end + + it "The action can access public methods" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_method + end + EOM + expect(ActionJackson.ran_action).to eq :access_method + expect(ActionJackson.succeeded).to eq 'foo_public!' + end + + it "The action can access protected methods" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_protected_method + end + EOM + expect(ActionJackson.ran_action).to eq :access_protected_method + expect(ActionJackson.succeeded).to eq 'foo_protected!' + end + + it "The action cannot access private methods" do + expect { + converge(<<-EOM, __FILE__, __LINE__+1) + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_private_method + end + EOM + }.to raise_error(NameError) + expect(ActionJackson.ran_action).to eq :access_private_method + end + + it "The action cannot access resource instance variables" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_instance_variable + end + EOM + expect(ActionJackson.ran_action).to eq :access_instance_variable + expect(ActionJackson.succeeded).to be_nil + end + + it "The action does not compile until the prior resource has converged" do + converge <<-EOM, __FILE__, __LINE__+1 + ruby_block 'wow' do + block do + ActionJackson.ruby_block_converged = 'ruby_block_converged!' + end + end + + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_class_method + end + EOM + expect(ActionJackson.ran_action).to eq :access_class_method + expect(ActionJackson.succeeded).to eq 'ruby_block_converged!' + end + + it "The action's resources converge before the next resource converges" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_attribute + end + + ruby_block 'wow' do + block do + ActionJackson.ruby_block_converged = ActionJackson.succeeded + end + end + EOM + expect(ActionJackson.ran_action).to eq :access_attribute + expect(ActionJackson.succeeded).to eq 'foo!' + expect(ActionJackson.ruby_block_converged).to eq 'foo!' + end + end + + context "With resource 'action_jackson'" do + before(:context) { + class ActionJackson < Chef::Resource + use_automatic_resource_name + def foo(value=nil) + @foo = value if value + @foo + end + def blarghle(value=nil) + @blarghle = value if value + @blarghle + end + + class <<self + attr_accessor :ran_action + attr_accessor :succeeded + attr_accessor :ruby_block_converged + end + + public + def foo_public + 'foo_public!' + end + protected + def foo_protected + 'foo_protected!' + end + private + def foo_private + 'foo_private!' + end + + public + action :access_recipe_dsl do + ActionJackson.ran_action = :access_recipe_dsl + ruby_block 'hi there' do + block do + ActionJackson.succeeded = true + end + end + end + action :access_attribute do + ActionJackson.ran_action = :access_attribute + ActionJackson.succeeded = foo + ActionJackson.succeeded += " #{blarghle}" if blarghle + ActionJackson.succeeded += " #{bar}" if respond_to?(:bar) + end + action :access_attribute2 do + ActionJackson.ran_action = :access_attribute2 + ActionJackson.succeeded = foo + ActionJackson.succeeded += " #{blarghle}" if blarghle + ActionJackson.succeeded += " #{bar}" if respond_to?(:bar) + end + action :access_method do + ActionJackson.ran_action = :access_method + ActionJackson.succeeded = foo_public + end + action :access_protected_method do + ActionJackson.ran_action = :access_protected_method + ActionJackson.succeeded = foo_protected + end + action :access_private_method do + ActionJackson.ran_action = :access_private_method + ActionJackson.succeeded = foo_private + end + action :access_instance_variable do + ActionJackson.ran_action = :access_instance_variable + ActionJackson.succeeded = @foo + end + action :access_class_method do + ActionJackson.ran_action = :access_class_method + ActionJackson.succeeded = ActionJackson.ruby_block_converged + end + end + } + before(:each) { + ActionJackson.ran_action = :error + ActionJackson.succeeded = :error + ActionJackson.ruby_block_converged = :error + } + + it_behaves_like "ActionJackson" do + let(:resource_dsl) { :action_jackson } + end + + context "And 'action_jackgrandson' inheriting from ActionJackson and changing nothing" do + before(:context) { + class ActionJackgrandson < ActionJackson + use_automatic_resource_name + end + } + + it_behaves_like "ActionJackson" do + let(:resource_dsl) { :action_jackgrandson } + end + end + + context "And 'action_jackalope' inheriting from ActionJackson with an extra attribute and action" do + before(:context) { + class ActionJackalope < ActionJackson + use_automatic_resource_name + + def foo(value=nil) + @foo = "#{value}alope" if value + @foo + end + def bar(value=nil) + @bar = "#{value}alope" if value + @bar + end + class <<self + attr_accessor :jackalope_ran + end + action :access_jackalope do + ActionJackalope.jackalope_ran = :access_jackalope + ActionJackalope.succeeded = "#{foo} #{blarghle} #{bar}" + end + action :access_attribute do + super() + ActionJackalope.jackalope_ran = :access_attribute + ActionJackalope.succeeded = ActionJackson.succeeded + end + end + } + before do + ActionJackalope.jackalope_ran = nil + end + + context "action_jackson still behaves the same" do + it_behaves_like "ActionJackson" do + let(:resource_dsl) { :action_jackson } + end + end + + it "The default action remains the same even though new actions were specified first" do + converge { + action_jackalope 'hi' do + foo 'foo!' + bar 'bar!' + end + } + expect(ActionJackson.ran_action).to eq :access_recipe_dsl + expect(ActionJackson.succeeded).to eq true + end + + it "new actions run, and can access overridden, new, and overridden attributes" do + converge { + action_jackalope 'hi' do + foo 'foo!' + bar 'bar!' + blarghle 'blarghle!' + action :access_jackalope + end + } + expect(ActionJackalope.jackalope_ran).to eq :access_jackalope + expect(ActionJackalope.succeeded).to eq "foo!alope blarghle! bar!alope" + end + + it "overridden actions run, call super, and can access overridden, new, and overridden attributes" do + converge { + action_jackalope 'hi' do + foo 'foo!' + bar 'bar!' + blarghle 'blarghle!' + action :access_attribute + end + } + expect(ActionJackson.ran_action).to eq :access_attribute + expect(ActionJackson.succeeded).to eq "foo!alope blarghle! bar!alope" + expect(ActionJackalope.jackalope_ran).to eq :access_attribute + expect(ActionJackalope.succeeded).to eq "foo!alope blarghle! bar!alope" + end + + it "non-overridden actions run and can access overridden and non-overridden variables (but not necessarily new ones)" do + converge { + action_jackalope 'hi' do + foo 'foo!' + bar 'bar!' + blarghle 'blarghle!' + action :access_attribute2 + end + } + expect(ActionJackson.ran_action).to eq :access_attribute2 + expect(ActionJackson.succeeded).to eq("foo!alope blarghle! bar!alope").or(eq("foo!alope blarghle!")) + end + end + end + + context "With a resource with no actions" do + before(:context) { + class NoActionJackson < Chef::Resource + use_automatic_resource_name + + def foo(value=nil) + @foo = value if value + @foo + end + + class <<self + attr_accessor :action_was + end + end + } + it "The default action is :nothing" do + converge { + no_action_jackson 'hi' do + foo 'foo!' + NoActionJackson.action_was = action + end + } + expect(NoActionJackson.action_was).to eq :nothing + end + end +end diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb index 1e4bbb5c56..8146774764 100644 --- a/spec/unit/client_spec.rb +++ b/spec/unit/client_spec.rb @@ -238,23 +238,24 @@ describe Chef::Client do describe "when converge completes successfully" do include_context "a client run" include_context "converge completed" - - describe "when audit phase errors" do - include_context "audit phase failed with error" - include_examples "a completed run with audit failure" do - let(:run_errors) { [audit_error] } + context 'when audit mode is enabled' do + describe "when audit phase errors" do + include_context "audit phase failed with error" + include_examples "a completed run with audit failure" do + let(:run_errors) { [audit_error] } + end end - end - describe "when audit phase completed" do - include_context "audit phase completed" - include_examples "a completed run" - end + describe "when audit phase completed" do + include_context "audit phase completed" + include_examples "a completed run" + end - describe "when audit phase completed with failed controls" do - include_context "audit phase completed with failed controls" - include_examples "a completed run with audit failure" do - let(:run_errors) { [audit_error] } + describe "when audit phase completed with failed controls" do + include_context "audit phase completed with failed controls" + include_examples "a completed run with audit failure" do + let(:run_errors) { [audit_error] } + end end end end @@ -512,11 +513,26 @@ describe Chef::Client do allow_any_instance_of(Chef::RunLock).to receive(:save_pid).and_raise(NoMethodError) end - it "should run exception handlers on early fail" do - expect(subject).to receive(:run_failed) - expect { subject.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| - expect(error.wrapped_errors.size).to eq 1 - expect(error.wrapped_errors).to include(NoMethodError) + context 'when audit mode is enabled' do + before do + Chef::Config[:audit_mode] = :enabled + end + it "should run exception handlers on early fail" do + expect(subject).to receive(:run_failed) + expect { subject.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| + expect(error.wrapped_errors.size).to eq 1 + expect(error.wrapped_errors).to include(NoMethodError) + end + end + end + + context 'when audit mode is disabled' do + before do + Chef::Config[:audit_mode] = :disabled + end + it "should run exception handlers on early fail" do + expect(subject).to receive(:run_failed) + expect { subject.run }.to raise_error(NoMethodError) end end end diff --git a/spec/unit/property/state_spec.rb b/spec/unit/property/state_spec.rb new file mode 100644 index 0000000000..80ebe01a41 --- /dev/null +++ b/spec/unit/property/state_spec.rb @@ -0,0 +1,491 @@ +require 'support/shared/integration/integration_helper' + +describe "Chef::Resource#identity and #state" do + include IntegrationSupport + + class NewResourceNamer + @i = 0 + def self.next + "chef_resource_property_spec_#{@i += 1}" + end + end + + def self.new_resource_name + NewResourceNamer.next + end + + let(:resource_class) do + new_resource_name = self.class.new_resource_name + Class.new(Chef::Resource) do + resource_name new_resource_name + end + end + + let(:resource) do + resource_class.new("blah") + end + + def self.english_join(values) + return '<nothing>' if values.size == 0 + return values[0].inspect if values.size == 1 + "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}" + end + + def self.with_property(*properties, &block) + tags_index = properties.find_index { |p| !p.is_a?(String)} + if tags_index + properties, tags = properties[0..tags_index-1], properties[tags_index..-1] + else + tags = [] + end + properties = properties.map { |property| "property #{property}" } + context "With properties #{english_join(properties)}", *tags do + before do + properties.each do |property_str| + resource_class.class_eval(property_str, __FILE__, __LINE__) + end + end + instance_eval(&block) + end + end + + # identity + context "Chef::Resource#identity_attr" do + with_property ":x" do + # it "name is the default identity" do + # expect(resource_class.identity_attr).to eq :name + # expect(resource_class.properties[:name].identity?).to be_falsey + # expect(resource.name).to eq 'blah' + # expect(resource.identity).to eq 'blah' + # end + + it "identity_attr :x changes the identity" do + expect(resource_class.identity_attr :x).to eq :x + expect(resource_class.identity_attr).to eq :x + # expect(resource_class.properties[:name].identity?).to be_falsey + # expect(resource_class.properties[:x].identity?).to be_truthy + + expect(resource.x 'woo').to eq 'woo' + expect(resource.x).to eq 'woo' + + expect(resource.name).to eq 'blah' + expect(resource.identity).to eq 'woo' + end + + # with_property ":y, identity: true" do + # context "and identity_attr :x" do + # before do + # resource_class.class_eval do + # identity_attr :x + # end + # end + # + # it "only returns :x as identity" do + # resource.x 'foo' + # resource.y 'bar' + # expect(resource_class.identity_attr).to eq :x + # expect(resource.identity).to eq 'foo' + # end + # it "does not flip y.desired_state off" do + # resource.x 'foo' + # resource.y 'bar' + # expect(resource_class.state_attrs).to eq [ :x, :y ] + # expect(resource.state).to eq({ x: 'foo', y: 'bar' }) + # end + # end + # end + + context "With a subclass" do + let(:subresource_class) do + new_resource_name = self.class.new_resource_name + Class.new(resource_class) do + resource_name new_resource_name + end + end + let(:subresource) do + subresource_class.new('sub') + end + + it "name is the default identity on the subclass" do + # expect(subresource_class.identity_attr).to eq :name + # expect(subresource_class.properties[:name].identity?).to be_falsey + expect(subresource.name).to eq 'sub' + expect(subresource.identity).to eq 'sub' + end + + context "With identity_attr :x on the superclass" do + before do + resource_class.class_eval do + identity_attr :x + end + end + + it "The subclass inherits :x as identity" do + expect(subresource_class.identity_attr).to eq :x + # expect(subresource_class.properties[:name].identity?).to be_falsey + # expect(subresource_class.properties[:x].identity?).to be_truthy + + subresource.x 'foo' + expect(subresource.identity).to eq 'foo' + end + + # context "With property :y, identity: true on the subclass" do + # before do + # subresource_class.class_eval do + # property :y, identity: true + # end + # end + # it "The subclass's identity includes both x and y" do + # expect(subresource_class.identity_attr).to eq :x + # subresource.x 'foo' + # subresource.y 'bar' + # expect(subresource.identity).to eq({ x: 'foo', y: 'bar' }) + # end + # end + + with_property ":y, String" do + context "With identity_attr :y on the subclass" do + before do + subresource_class.class_eval do + identity_attr :y + end + end + # it "y is part of state" do + # subresource.x 'foo' + # subresource.y 'bar' + # expect(subresource.state).to eq({ x: 'foo', y: 'bar' }) + # expect(subresource_class.state_attrs).to eq [ :x, :y ] + # end + it "y is the identity" do + expect(subresource_class.identity_attr).to eq :y + subresource.x 'foo' + subresource.y 'bar' + expect(subresource.identity).to eq 'bar' + end + it "y still has validation" do + expect { subresource.y 12 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + end + end + end + end + + # with_property ":string_only, String, identity: true", ":string_only2, String" do + # it "identity_attr does not change validation" do + # resource_class.identity_attr :string_only + # expect { resource.string_only 12 }.to raise_error Chef::Exceptions::ValidationFailed + # expect { resource.string_only2 12 }.to raise_error Chef::Exceptions::ValidationFailed + # end + # end + # + # with_property ":x, desired_state: false" do + # it "identity_attr does not flip on desired_state" do + # resource_class.identity_attr :x + # resource.x 'hi' + # expect(resource.identity).to eq 'hi' + # # expect(resource_class.properties[:x].desired_state?).to be_falsey + # expect(resource_class.state_attrs).to eq [] + # expect(resource.state).to eq({}) + # end + # end + + context "With custom property custom_property defined only as methods, using different variables for storage" do + before do + resource_class.class_eval do + def custom_property + @blarghle ? @blarghle*3 : nil + end + def custom_property=(x) + @blarghle = x*2 + end + end + end + + context "And identity_attr :custom_property" do + before do + resource_class.class_eval do + identity_attr :custom_property + end + end + + it "identity_attr comes back as :custom_property" do + # expect(resource_class.properties[:custom_property].identity?).to be_truthy + expect(resource_class.identity_attr).to eq :custom_property + end + # it "custom_property becomes part of desired_state" do + # resource.custom_property = 1 + # expect(resource.state).to eq({ custom_property: 6 }) + # expect(resource_class.properties[:custom_property].desired_state?).to be_truthy + # expect(resource_class.state_attrs).to eq [ :custom_property ] + # end + it "identity_attr does not change custom_property's getter or setter" do + resource.custom_property = 1 + expect(resource.custom_property).to eq 6 + end + it "custom_property is returned as the identity" do + expect(resource.identity).to be_nil + resource.custom_property = 1 + expect(resource.identity).to eq 6 + end + end + end + end + + # context "PropertyType#identity" do + # with_property ":x, identity: true" do + # it "name is only part of the identity if an identity attribute is defined" do + # expect(resource_class.identity_attr).to eq :x + # resource.x 'woo' + # expect(resource.identity).to eq 'woo' + # end + # end + # + # with_property ":x, identity: true, default: 'xxx'", + # ":y, identity: true, default: 'yyy'", + # ":z, identity: true, default: 'zzz'" do + # it "identity_attr returns the first identity attribute if multiple are defined" do + # expect(resource_class.identity_attr).to eq :x + # end + # it "identity returns all identity values in a hash if multiple are defined" do + # resource.x 'foo' + # resource.y 'bar' + # resource.z 'baz' + # expect(resource.identity).to eq({ x: 'foo', y: 'bar', z: 'baz' }) + # end + # it "identity returns only identity values that are set, and does not include defaults" do + # resource.x 'foo' + # resource.z 'baz' + # expect(resource.identity).to eq({ x: 'foo', z: 'baz' }) + # end + # it "identity returns only set identity values in a hash, if there is only one set identity value" do + # resource.x 'foo' + # expect(resource.identity).to eq({ x: 'foo' }) + # end + # it "identity returns an empty hash if no identity values are set" do + # expect(resource.identity).to eq({}) + # end + # it "identity_attr wipes out any other identity attributes if multiple are defined" do + # resource_class.identity_attr :y + # resource.x 'foo' + # resource.y 'bar' + # resource.z 'baz' + # expect(resource.identity).to eq 'bar' + # end + # end + # + # with_property ":x, identity: true, name_property: true" do + # it "identity when x is not defined returns the value of x" do + # expect(resource.identity).to eq 'blah' + # end + # it "state when x is not defined returns the value of x" do + # expect(resource.state).to eq({ x: 'blah' }) + # end + # end + # end + + # state_attrs + context "Chef::Resource#state_attrs" do + it "name is not part of state_attrs" do + expect(Chef::Resource.state_attrs).to eq [] + expect(resource_class.state_attrs).to eq [] + expect(resource.state).to eq({}) + end + + # with_property ":x", ":y", ":z" do + # it "x, y and z are state attributes" do + # resource.x 1 + # resource.y 2 + # resource.z 3 + # expect(resource_class.state_attrs).to eq [ :x, :y, :z ] + # expect(resource.state).to eq(x: 1, y: 2, z: 3) + # end + # it "values that are not set are not included in state" do + # resource.x 1 + # expect(resource.state).to eq(x: 1) + # end + # it "when no values are set, nothing is included in state" do + # end + # end + # + # with_property ":x", ":y, desired_state: false", ":z, desired_state: true" do + # it "x and z are state attributes, and y is not" do + # resource.x 1 + # resource.y 2 + # resource.z 3 + # expect(resource_class.state_attrs).to eq [ :x, :z ] + # expect(resource.state).to eq(x: 1, z: 3) + # end + # end + + # with_property ":x, name_property: true" do + # it "Unset values with name_property are included in state" do + # expect(resource.state).to eq(x: 'blah') + # end + # it "Set values with name_property are included in state" do + # resource.x 1 + # expect(resource.state).to eq(x: 1) + # end + # end + + # with_property ":x, default: 1" do + # it "Unset values with defaults are not included in state" do + # expect(resource.state).to eq({}) + # end + # it "Set values with defaults are included in state" do + # resource.x 1 + # expect(resource.state).to eq(x: 1) + # end + # end + + context "With a class with a normal getter and setter" do + before do + resource_class.class_eval do + def x + @blah*3 + end + def x=(value) + @blah = value*2 + end + end + end + it "state_attrs(:x) causes the value to be included in properties" do + resource_class.state_attrs(:x) + resource.x = 1 + + expect(resource.x).to eq 6 + expect(resource.state).to eq(x: 6) + end + end + + # with_property ":x, Integer, identity: true" do + # it "state_attrs(:x) leaves the property in desired_state" do + # resource_class.state_attrs(:x) + # resource.x 10 + # + # # expect(resource_class.properties[:x].desired_state?).to be_truthy + # expect(resource_class.state_attrs).to eq [ :x ] + # expect(resource.state).to eq(x: 10) + # end + # it "state_attrs(:x) does not turn off validation" do + # resource_class.state_attrs(:x) + # expect { resource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed + # end + # it "state_attrs(:x) does not turn off identity" do + # resource_class.state_attrs(:x) + # resource.x 10 + # + # expect(resource_class.identity_attr).to eq :x + # # expect(resource_class.properties[:x].identity?).to be_truthy + # expect(resource.identity).to eq 10 + # end + # end + + # with_property ":x, Integer, identity: true, desired_state: false" do + # before do + # resource_class.class_eval do + # def y + # 20 + # end + # end + # end + # it "state_attrs(:x) sets the property in desired_state" do + # resource_class.state_attrs(:x) + # resource.x 10 + # + # # expect(resource_class.properties[:x].desired_state?).to be_truthy + # expect(resource_class.state_attrs).to eq [ :x ] + # expect(resource.state).to eq(x: 10) + # end + # it "state_attrs(:x) does not turn off validation" do + # resource_class.state_attrs(:x) + # expect { resource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed + # end + # it "state_attrs(:x) does not turn off identity" do + # resource_class.state_attrs(:x) + # resource.x 10 + # + # expect(resource_class.identity_attr).to eq :x + # # expect(resource_class.properties[:x].identity?).to be_truthy + # expect(resource.identity).to eq 10 + # end + # it "state_attrs(:y) adds y and removes x from desired state" do + # resource_class.state_attrs(:y) + # resource.x 10 + # + # # expect(resource_class.properties[:x].desired_state?).to be_falsey + # # expect(resource_class.properties[:y].desired_state?).to be_truthy + # expect(resource_class.state_attrs).to eq [ :y ] + # expect(resource.state).to eq(y: 20) + # end + # it "state_attrs(:y) does not turn off validation" do + # resource_class.state_attrs(:y) + # + # expect { resource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed + # end + # it "state_attrs(:y) does not turn off identity" do + # resource_class.state_attrs(:y) + # resource.x 10 + # + # expect(resource_class.identity_attr).to eq :x + # # expect(resource_class.properties[:x].identity?).to be_truthy + # expect(resource.identity).to eq 10 + # end + # + # context "With a subclassed resource" do + # let(:resource_subclass) do + # new_resource_name = self.class.new_resource_name + # Class.new(resource_class) do + # resource_name new_resource_name + # end + # end + # let(:subresource) do + # resource_subclass.new('blah') + # end + # it "state_attrs(:x) sets the property in desired_state" do + # resource_subclass.state_attrs(:x) + # subresource.x 10 + # + # # expect(resource_subclass.properties[:x].desired_state?).to be_truthy + # expect(resource_subclass.state_attrs).to eq [ :x ] + # expect(subresource.state).to eq(x: 10) + # end + # it "state_attrs(:x) does not turn off validation" do + # resource_subclass.state_attrs(:x) + # expect { subresource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed + # end + # it "state_attrs(:x) does not turn off identity" do + # resource_subclass.state_attrs(:x) + # subresource.x 10 + # + # expect(resource_subclass.identity_attr).to eq :x + # # expect(resource_subclass.properties[:x].identity?).to be_truthy + # expect(subresource.identity).to eq 10 + # end + # it "state_attrs(:y) adds y and removes x from desired state" do + # resource_subclass.state_attrs(:y) + # subresource.x 10 + # + # # expect(resource_subclass.properties[:x].desired_state?).to be_falsey + # # expect(resource_subclass.properties[:y].desired_state?).to be_truthy + # expect(resource_subclass.state_attrs).to eq [ :y ] + # expect(subresource.state).to eq(y: 20) + # end + # it "state_attrs(:y) does not turn off validation" do + # resource_subclass.state_attrs(:y) + # + # expect { subresource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed + # end + # it "state_attrs(:y) does not turn off identity" do + # resource_subclass.state_attrs(:y) + # subresource.x 10 + # + # expect(resource_subclass.identity_attr).to eq :x + # # expect(resource_subclass.properties[:x].identity?).to be_truthy + # expect(subresource.identity).to eq 10 + # end + # end + # end + end + +end diff --git a/spec/unit/property/validation_spec.rb b/spec/unit/property/validation_spec.rb new file mode 100644 index 0000000000..e32147bd38 --- /dev/null +++ b/spec/unit/property/validation_spec.rb @@ -0,0 +1,652 @@ +require 'support/shared/integration/integration_helper' + +describe "Chef::Resource.property validation" do + include IntegrationSupport + + module Namer + @i = 0 + def self.next_resource_name + "chef_resource_property_spec_#{@i += 1}" + end + def self.reset_index + @current_index = 0 + end + def self.current_index + @current_index + end + def self.next_index + @current_index += 1 + end + end + + def lazy(&block) + Chef::DelayedEvaluator.new(&block) + end + + before do + Namer.reset_index + end + + def self.new_resource_name + Namer.next_resource_name + end + + let(:resource_class) do + new_resource_name = self.class.new_resource_name + Class.new(Chef::Resource) do + resource_name new_resource_name + def blah + Namer.next_index + end + def self.blah + "class#{Namer.next_index}" + end + end + end + + let(:resource) do + resource_class.new("blah") + end + + def self.english_join(values) + return '<nothing>' if values.size == 0 + return values[0].inspect if values.size == 1 + "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}" + end + + def self.with_property(*properties, &block) + tags_index = properties.find_index { |p| !p.is_a?(String)} + if tags_index + properties, tags = properties[0..tags_index-1], properties[tags_index..-1] + else + tags = [] + end + properties = properties.map { |property| "property #{property}" } + context "With properties #{english_join(properties)}", *tags do + before do + properties.each do |property_str| + resource_class.class_eval(property_str, __FILE__, __LINE__) + end + end + instance_eval(&block) + end + end + + def self.validation_test(validation, success_values, failure_values, getter_values=[], *tags) + with_property ":x, #{validation}", *tags do + it "gets nil when retrieving the initial (non-set) value" do + expect(resource.x).to be_nil + end + success_values.each do |v| + it "value #{v.inspect} is valid" do + resource.instance_eval { @x = 'default' } + expect(resource.x v).to eq v + expect(resource.x).to eq v + end + end + failure_values.each do |v| + it "value #{v.inspect} is invalid" do + expect { resource.x v }.to raise_error Chef::Exceptions::ValidationFailed + resource.instance_eval { @x = 'default' } + expect { resource.x v }.to raise_error Chef::Exceptions::ValidationFailed + end + end + getter_values.each do |v| + it "setting value to #{v.inspect} does not change the value" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.instance_eval { @x = 'default' } + expect(resource.x v).to eq 'default' + expect(resource.x).to eq 'default' + end + end + end + end + + context "basic get, set, and nil set" do + with_property ":x, kind_of: String" do + context "when the variable already has a value" do + before do + resource.instance_eval { @x = 'default' } + end + it "get succeeds" do + expect(resource.x).to eq 'default' + end + it "set(nil) = get" do + expect(resource.x nil).to eq 'default' + expect(resource.x).to eq 'default' + end + it "set to valid value succeeds" do + expect(resource.x 'str').to eq 'str' + expect(resource.x).to eq 'str' + end + it "set to invalid value raises ValidationFailed" do + expect { resource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + context "when the variable does not have an initial value" do + it "get succeeds" do + expect(resource.x).to be_nil + end + it "set(nil) = get" do + expect(resource.x nil).to be_nil + expect(resource.x).to be_nil + end + it "set to valid value succeeds" do + expect(resource.x 'str').to eq 'str' + expect(resource.x).to eq 'str' + end + it "set to invalid value raises ValidationFailed" do + expect { resource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + end + with_property ":x, [ String, nil ]" do + context "when the variable already has a value" do + before do + resource.instance_eval { @x = 'default' } + end + it "get succeeds" do + expect(resource.x).to eq 'default' + end + it "set(nil) sets the value" do + expect(resource.x nil).to be_nil + expect(resource.x).to be_nil + end + it "set to valid value succeeds" do + expect(resource.x 'str').to eq 'str' + expect(resource.x).to eq 'str' + end + it "set to invalid value raises ValidationFailed" do + expect { resource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + context "when the variable does not have an initial value" do + it "get succeeds" do + expect(resource.x).to be_nil + end + it "set(nil) sets the value" do + expect(resource.x nil).to be_nil + expect(resource.x).to be_nil + end + it "set to valid value succeeds" do + expect(resource.x 'str').to eq 'str' + expect(resource.x).to eq 'str' + end + it "set to invalid value raises ValidationFailed" do + expect { resource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + end + end + + # Bare types + context "bare types" do + validation_test 'String', + [ 'hi' ], + [ 10 ], + [ nil ] + + validation_test ':a', + [ :a ], + [ :b ], + [ nil ] + + validation_test ':a, is: :b', + [ :a, :b ], + [ :c ], + [ nil ] + + validation_test ':a, is: [ :b, :c ]', + [ :a, :b, :c ], + [ :d ], + [ nil ] + + validation_test '[ :a, :b ], is: :c', + [ :a, :b, :c ], + [ :d ], + [ nil ] + + validation_test '[ :a, :b ], is: [ :c, :d ]', + [ :a, :b, :c, :d ], + [ :e ], + [ nil ] + + validation_test 'nil', + [ nil ], + [ :a ] + + validation_test '[ nil ]', + [ nil ], + [ :a ] + + validation_test '[]', + [], + [ :a ], + [ nil ] + end + + # is + context "is" do + # Class + validation_test 'is: String', + [ 'a', '' ], + [ :a, 1 ], + [ nil ] + + # Value + validation_test 'is: :a', + [ :a ], + [ :b ], + [ nil ] + + validation_test 'is: [ :a, :b ]', + [ :a, :b ], + [ [ :a, :b ] ], + [ nil ] + + validation_test 'is: [ [ :a, :b ] ]', + [ [ :a, :b ] ], + [ :a, :b ], + [ nil ] + + # Regex + validation_test 'is: /abc/', + [ 'abc', 'wowabcwow' ], + [ '', 'abac' ], + [ nil ] + + # PropertyType + # validation_test 'is: PropertyType.new(is: :a)', + # [ :a ], + # [ :b, nil ] + + # RSpec Matcher + class Globalses + extend RSpec::Matchers + end + + validation_test "is: Globalses.eq(10)", + [ 10 ], + [ 1 ], + [ nil ] + + # Proc + validation_test 'is: proc { |x| x }', + [ true, 1 ], + [ false ], + [ nil ] + + validation_test 'is: proc { |x| x > blah }', + [ 10 ], + [ -1 ] + + validation_test 'is: nil', + [ nil ], + [ 'a' ] + + validation_test 'is: [ String, nil ]', + [ 'a', nil ], + [ :b ] + + validation_test 'is: []', + [], + [ :a ], + [ nil ] + end + + # Combination + context "combination" do + validation_test 'kind_of: String, equal_to: "a"', + [ 'a' ], + [ 'b' ], + [ nil ] + end + + # equal_to + context "equal_to" do + # Value + validation_test 'equal_to: :a', + [ :a ], + [ :b ], + [ nil ] + + validation_test 'equal_to: [ :a, :b ]', + [ :a, :b ], + [ [ :a, :b ] ], + [ nil ] + + validation_test 'equal_to: [ [ :a, :b ] ]', + [ [ :a, :b ] ], + [ :a, :b ], + [ nil ] + + validation_test 'equal_to: nil', + [ ], + [ 'a' ], + [ nil ] + + validation_test 'equal_to: [ "a", nil ]', + [ 'a' ], + [ 'b' ], + [ nil ] + + validation_test 'equal_to: [ nil, "a" ]', + [ 'a' ], + [ 'b' ], + [ nil ] + + validation_test 'equal_to: []', + [], + [ :a ], + [ nil ] + end + + # kind_of + context "kind_of" do + validation_test 'kind_of: String', + [ 'a' ], + [ :b ], + [ nil ] + + validation_test 'kind_of: [ String, Symbol ]', + [ 'a', :b ], + [ 1 ], + [ nil ] + + validation_test 'kind_of: [ Symbol, String ]', + [ 'a', :b ], + [ 1 ], + [ nil ] + + validation_test 'kind_of: NilClass', + [ ], + [ 'a' ], + [ nil ] + + validation_test 'kind_of: [ NilClass, String ]', + [ 'a' ], + [ :a ], + [ nil ] + + validation_test 'kind_of: []', + [], + [ :a ], + [ nil ] + + validation_test 'kind_of: nil', + [], + [ :a ], + [ nil ] + end + + # regex + context "regex" do + validation_test 'regex: /abc/', + [ 'xabcy' ], + [ 'gbh', 123 ], + [ nil ] + + validation_test 'regex: [ /abc/, /z/ ]', + [ 'xabcy', 'aza' ], + [ 'gbh', 123 ], + [ nil ] + + validation_test 'regex: [ /z/, /abc/ ]', + [ 'xabcy', 'aza' ], + [ 'gbh', 123 ], + [ nil ] + + validation_test 'regex: []', + [], + [ :a ], + [ nil ] + + validation_test 'regex: nil', + [], + [ :a ], + [ nil ] + end + + # callbacks + context "callbacks" do + validation_test 'callbacks: { "a" => proc { |x| x > 10 }, "b" => proc { |x| x%2 == 0 } }', + [ 12 ], + [ 11, 4 ] + + validation_test 'callbacks: { "a" => proc { |x| x%2 == 0 }, "b" => proc { |x| x > 10 } }', + [ 12 ], + [ 11, 4 ] + + validation_test 'callbacks: { "a" => proc { |x| x.nil? } }', + [ ], + [ 'a' ], + [ nil ] + + validation_test 'callbacks: {}', + [ :a ], + [], + [ nil ] + end + + # respond_to + context "respond_to" do + validation_test 'respond_to: :split', + [ 'hi' ], + [ 1 ], + [ nil ] + + validation_test 'respond_to: "split"', + [ 'hi' ], + [ 1 ], + [ nil ] + + validation_test 'respond_to: :to_s', + [ :a ], + [], + [ nil ] + + validation_test 'respond_to: [ :split, :to_s ]', + [ 'hi' ], + [ 1 ], + [ nil ] + + validation_test 'respond_to: %w(split to_s)', + [ 'hi' ], + [ 1 ], + [ nil ] + + validation_test 'respond_to: [ :to_s, :split ]', + [ 'hi' ], + [ 1, ], + [ nil ] + + validation_test 'respond_to: []', + [ :a ], + [], + [ nil ] + + validation_test 'respond_to: nil', + [ :a ], + [], + [ nil ] + end + + context "cannot_be" do + validation_test 'cannot_be: :empty', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: "empty"', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: [ :empty, :nil ]', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: [ "empty", "nil" ]', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: [ :nil, :empty ]', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: [ :empty, :nil, :blahblah ]', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: []', + [ :a ], + [], + [ nil ] + + validation_test 'cannot_be: nil', + [ :a ], + [], + [ nil ] + + end + + context "required" do + with_property ':x, required: true' do + it "if x is not specified, retrieval fails" do + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + end + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + it "value nil does a get" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x 1 + resource.x nil + expect(resource.x).to eq 1 + end + end + + with_property ':x, [String, nil], required: true' do + it "if x is not specified, retrieval fails" do + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + end + it "value nil is valid" do + expect(resource.x nil).to be_nil + expect(resource.x).to be_nil + end + it "value '1' is valid" do + expect(resource.x '1').to eq '1' + expect(resource.x).to eq '1' + end + it "value 1 is invalid" do + expect { resource.x 1 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + + with_property ':x, name_property: true, required: true' do + it "if x is not specified, retrieval fails" do + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + end + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + it "value nil does a get" do + resource.x 1 + resource.x nil + expect(resource.x).to eq 1 + end + end + + with_property ':x, default: 10, required: true' do + it "if x is not specified, retrieval fails" do + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + end + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + it "value nil does a get" do + resource.x 1 + resource.x nil + expect(resource.x).to eq 1 + end + end + end + + context "custom validators (def _pv_blarghle)" do + before do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + end + + with_property ':x, blarghle: 1' do + context "and a class that implements _pv_blarghle" do + before do + resource_class.class_eval do + def _pv_blarghle(opts, key, value) + if _pv_opts_lookup(opts, key) != value + raise Chef::Exceptions::ValidationFailed, "ouch" + end + end + end + end + + # it "getting the value causes a deprecation warning" do + # Chef::Config[:treat_deprecation_warnings_as_errors] = true + # expect { resource.x }.to raise_error Chef::Exceptions::DeprecatedFeatureError + # end + + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + + it "value '1' is invalid" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + expect { resource.x '1' }.to raise_error Chef::Exceptions::ValidationFailed + end + + it "value nil does a get" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x 1 + resource.x nil + expect(resource.x).to eq 1 + end + end + end + + with_property ':x, blarghle: 1' do + context "and a class that implements _pv_blarghle" do + before do + resource_class.class_eval do + def _pv_blarghle(opts, key, value) + if _pv_opts_lookup(opts, key) != value + raise Chef::Exceptions::ValidationFailed, "ouch" + end + end + end + end + + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + + it "value '1' is invalid" do + expect { resource.x '1' }.to raise_error Chef::Exceptions::ValidationFailed + end + + it "value nil does a get" do + resource.x 1 + resource.x nil + expect(resource.x).to eq 1 + end + end + end + end +end diff --git a/spec/unit/property_spec.rb b/spec/unit/property_spec.rb new file mode 100644 index 0000000000..ce0552c564 --- /dev/null +++ b/spec/unit/property_spec.rb @@ -0,0 +1,802 @@ +require 'support/shared/integration/integration_helper' + +describe "Chef::Resource.property" do + include IntegrationSupport + + module Namer + @i = 0 + def self.next_resource_name + "chef_resource_property_spec_#{@i += 1}" + end + def self.reset_index + @current_index = 0 + end + def self.current_index + @current_index + end + def self.next_index + @current_index += 1 + end + end + + def lazy(&block) + Chef::DelayedEvaluator.new(&block) + end + + before do + Namer.reset_index + end + + def self.new_resource_name + Namer.next_resource_name + end + + let(:resource_class) do + new_resource_name = self.class.new_resource_name + Class.new(Chef::Resource) do + resource_name new_resource_name + def next_index + Namer.next_index + end + end + end + + let(:resource) do + resource_class.new("blah") + end + + def self.english_join(values) + return '<nothing>' if values.size == 0 + return values[0].inspect if values.size == 1 + "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}" + end + + def self.with_property(*properties, &block) + tags_index = properties.find_index { |p| !p.is_a?(String)} + if tags_index + properties, tags = properties[0..tags_index-1], properties[tags_index..-1] + else + tags = [] + end + properties = properties.map { |property| "property #{property}" } + context "With properties #{english_join(properties)}", *tags do + before do + properties.each do |property_str| + resource_class.class_eval(property_str, __FILE__, __LINE__) + end + end + instance_eval(&block) + end + end + + # Basic properties + with_property ':bare_property' do + it "can be set" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property).to eq 10 + end + # it "emits a deprecation warning and does a get, if set to nil" do + it "emits a deprecation warning and does a get, if set to nil" do + expect(resource.bare_property 10).to eq 10 + # expect { resource.bare_property nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + # Chef::Config[:treat_deprecation_warnings_as_errors] = false + expect(resource.bare_property nil).to eq 10 + expect(resource.bare_property).to eq 10 + end + it "can be updated" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property 20).to eq 20 + expect(resource.bare_property).to eq 20 + end + it "can be set with =" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property).to eq 10 + end + # it "can be set to nil with =" do + # expect(resource.bare_property 10).to eq 10 + # expect(resource.bare_property = nil).to be_nil + # expect(resource.bare_property).to be_nil + # end + it "can be updated with =" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property = 20).to eq 20 + expect(resource.bare_property).to eq 20 + end + end + + with_property ":x, Integer" do + context "and subclass" do + let(:subresource_class) do + new_resource_name = self.class.new_resource_name + Class.new(resource_class) do + resource_name new_resource_name + end + end + let(:subresource) do + subresource_class.new('blah') + end + + it "x is inherited" do + expect(subresource.x 10).to eq 10 + expect(subresource.x).to eq 10 + expect(subresource.x = 20).to eq 20 + expect(subresource.x).to eq 20 + # expect(subresource_class.properties[:x]).not_to be_nil + end + + it "x's validation is inherited" do + expect { subresource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed + end + + context "with property :y on the subclass" do + before do + subresource_class.class_eval do + property :y + end + end + + it "x is still there" do + expect(subresource.x 10).to eq 10 + expect(subresource.x).to eq 10 + expect(subresource.x = 20).to eq 20 + expect(subresource.x).to eq 20 + # expect(subresource_class.properties[:x]).not_to be_nil + end + it "y is there" do + expect(subresource.y 10).to eq 10 + expect(subresource.y).to eq 10 + expect(subresource.y = 20).to eq 20 + expect(subresource.y).to eq 20 + # expect(subresource_class.properties[:y]).not_to be_nil + end + it "y is not on the superclass" do + expect { resource_class.y 10 }.to raise_error + # expect(resource_class.properties[:y]).to be_nil + end + end + + context "with property :x on the subclass" do + before do + subresource_class.class_eval do + property :x + end + end + + it "x is still there" do + expect(subresource.x 10).to eq 10 + expect(subresource.x).to eq 10 + expect(subresource.x = 20).to eq 20 + expect(subresource.x).to eq 20 + # expect(subresource_class.properties[:x]).not_to be_nil + # expect(subresource_class.properties[:x]).not_to eq resource_class.properties[:x] + end + + it "x's validation is overwritten" do + expect(subresource.x 'ohno').to eq 'ohno' + expect(subresource.x).to eq 'ohno' + end + + it "the superclass's validation for x is still there" do + expect { resource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed + end + end + + context "with property :x, String on the subclass" do + before do + subresource_class.class_eval do + property :x, String + end + end + + it "x is still there" do + expect(subresource.x "10").to eq "10" + expect(subresource.x).to eq "10" + expect(subresource.x = "20").to eq "20" + expect(subresource.x).to eq "20" + # expect(subresource_class.properties[:x]).not_to be_nil + # expect(subresource_class.properties[:x]).not_to eq resource_class.properties[:x] + end + + it "x's validation is overwritten" do + expect { subresource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed + expect(subresource.x 'ohno').to eq 'ohno' + expect(subresource.x).to eq 'ohno' + end + + it "the superclass's validation for x is still there" do + expect { resource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed + expect(resource.x 10).to eq 10 + expect(resource.x).to eq 10 + end + end + end + end + + context "Chef::Resource::PropertyType#property_is_set?" do + it "when a resource is newly created, property_is_set?(:name) is true" do + expect(resource.property_is_set?(:name)).to be_truthy + end + + # it "when referencing an undefined property, property_is_set?(:x) raises an error" do + # expect { resource.property_is_set?(:x) }.to raise_error(ArgumentError) + # end + + with_property ':x' do + it "when the resource is newly created, property_is_set?(:x) is false" do + expect(resource.property_is_set?(:x)).to be_falsey + end + it "when x is set, property_is_set?(:x) is true" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set with =, property_is_set?(:x) is true" do + resource.x = 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set to a lazy value, property_is_set?(:x) is true" do + resource.x lazy { 10 } + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is retrieved, property_is_set?(:x) is false" do + resource.x + expect(resource.property_is_set?(:x)).to be_falsey + end + end + + with_property ':x, default: 10' do + it "when the resource is newly created, property_is_set?(:x) is false" do + expect(resource.property_is_set?(:x)).to be_falsey + end + it "when x is set, property_is_set?(:x) is true" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set with =, property_is_set?(:x) is true" do + resource.x = 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set to a lazy value, property_is_set?(:x) is true" do + resource.x lazy { 10 } + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is retrieved, property_is_set?(:x) is true" do + resource.x + expect(resource.property_is_set?(:x)).to be_truthy + end + end + + with_property ':x, default: nil' do + it "when the resource is newly created, property_is_set?(:x) is false" do + expect(resource.property_is_set?(:x)).to be_falsey + end + it "when x is set, property_is_set?(:x) is true" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set with =, property_is_set?(:x) is true" do + resource.x = 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set to a lazy value, property_is_set?(:x) is true" do + resource.x lazy { 10 } + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is retrieved, property_is_set?(:x) is true" do + resource.x + expect(resource.property_is_set?(:x)).to be_truthy + end + end + + with_property ':x, default: lazy { 10 }' do + it "when the resource is newly created, property_is_set?(:x) is false" do + expect(resource.property_is_set?(:x)).to be_falsey + end + it "when x is set, property_is_set?(:x) is true" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set with =, property_is_set?(:x) is true" do + resource.x = 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is retrieved, property_is_set?(:x) is true" do + resource.x + expect(resource.property_is_set?(:x)).to be_truthy + end + end + end + + context "Chef::Resource::PropertyType#default" do + with_property ':x, default: 10' do + it "when x is set, it returns its value" do + expect(resource.x 20).to eq 20 + expect(resource.property_is_set?(:x)).to be_truthy + expect(resource.x).to eq 20 + end + it "when x is not set, it returns 10" do + expect(resource.x).to eq 10 + end + it "when x is not set, it is not included in state" do + expect(resource.state).to eq({}) + end + it "when x is set to nil, it returns nil" do + resource.instance_eval { @x = nil } + expect(resource.x).to be_nil + end + + context "With a subclass" do + let(:subresource_class) do + new_resource_name = self.class.new_resource_name + Class.new(resource_class) do + resource_name new_resource_name + end + end + let(:subresource) { subresource_class.new('blah') } + it "The default is inherited" do + expect(subresource.x).to eq 10 + end + end + end + + with_property ':x, default: 10, identity: true' do + it "when x is not set, it is not included in identity" do + expect(resource.state).to eq({}) + end + end + + with_property ':x, default: nil' do + it "when x is not set, it returns nil" do + expect(resource.x).to be_nil + end + end + + with_property ':x' do + it "when x is not set, it returns nil" do + expect(resource.x).to be_nil + end + end + + context "hash default" do + with_property ':x, default: {}' do + it "when x is not set, it returns {}" do + expect(resource.x).to eq({}) + end + it "The same exact value is returned multiple times in a row" do + value = resource.x + expect(value).to eq({}) + expect(resource.x.object_id).to eq(value.object_id) + end + it "Multiple instances of x receive the exact same value" do + expect(resource.x.object_id).to eq(resource_class.new('blah2').x.object_id) + end + it "The default value is frozen" do + expect(resource.x).to be_frozen + end + it "The default value cannot be written to" do + expect { resource.x[:a] = 1 }.to raise_error RuntimeError, /frozen/ + end + end + + with_property ':x, default: lazy { {} }' do + it "when x is not set, it returns {}" do + expect(resource.x).to eq({}) + end + # it "The value is different each time it is called" do + # value = resource.x + # expect(value).to eq({}) + # expect(resource.x.object_id).not_to eq(value.object_id) + # end + it "Multiple instances of x receive different values" do + expect(resource.x.object_id).not_to eq(resource_class.new('blah2').x.object_id) + end + end + end + + context "with a class with 'blah' as both class and instance methods" do + before do + resource_class.class_eval do + def self.blah + 'class' + end + def blah + "#{name}#{next_index}" + end + end + end + + with_property ':x, default: lazy { blah }' do + it "x is run in context of the instance" do + expect(resource.x).to eq "blah1" + end + it "x is run in the context of each instance it is run in" do + expect(resource.x).to eq "blah1" + expect(resource_class.new('another').x).to eq "another2" + # expect(resource.x).to eq "blah3" + end + end + + with_property ':x, default: lazy { |x| "#{blah}#{x.blah}" }' do + it "x is run in context of the class (where it was defined) and passed the instance" do + expect(resource.x).to eq "classblah1" + end + it "x is passed the value of each instance it is run in" do + expect(resource.x).to eq "classblah1" + expect(resource_class.new('another').x).to eq "classanother2" + # expect(resource.x).to eq "classblah3" + end + end + end + + context "validation of defaults" do + with_property ':x, String, default: 10' do + it "when the resource is created, no error is raised" do + resource + end + it "when x is set, no error is raised" do + expect(resource.x 'hi').to eq 'hi' + expect(resource.x).to eq 'hi' + end + it "when x is retrieved, no validation error is raised" do + expect(resource.x).to eq 10 + end + # it "when x is retrieved, a validation error is raised" do + # expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + # end + end + + with_property ":x, String, default: lazy { Namer.next_index }" do + it "when the resource is created, no error is raised" do + resource + end + it "when x is set, no error is raised" do + expect(resource.x 'hi').to eq 'hi' + expect(resource.x).to eq 'hi' + end + it "when x is retrieved, no validation error is raised" do + expect(resource.x).to eq 1 + expect(Namer.current_index).to eq 1 + end + # it "when x is retrieved, a validation error is raised" do + # expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + # expect(Namer.current_index).to eq 1 + # end + end + + with_property ":x, default: lazy { Namer.next_index }, is: proc { |v| Namer.next_index; true }" do + it "validation is not run at all on the default value" do + expect(resource.x).to eq 1 + expect(Namer.current_index).to eq 1 + end + # it "validation is only run the first time" do + # expect(resource.x).to eq 1 + # expect(Namer.current_index).to eq 2 + # expect(resource.x).to eq 1 + # expect(Namer.current_index).to eq 2 + # end + end + end + + context "coercion of defaults" do + with_property ':x, coerce: proc { |v| "#{v}#{next_index}" }, default: 10' do + it "when the resource is created, the proc is not yet run" do + resource + expect(Namer.current_index).to eq 0 + end + it "when x is set, coercion is run" do + expect(resource.x 'hi').to eq 'hi1' + expect(resource.x).to eq 'hi1' + expect(Namer.current_index).to eq 1 + end + it "when x is retrieved, coercion is run, no more than once" do + expect(resource.x).to eq '101' + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + end + end + + with_property ':x, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "when the resource is created, the proc is not yet run" do + resource + expect(Namer.current_index).to eq 0 + end + it "when x is set, coercion is run" do + expect(resource.x 'hi').to eq 'hi1' + expect(resource.x).to eq 'hi1' + expect(Namer.current_index).to eq 1 + end + end + + with_property ':x, proc { |v| Namer.next_index; true }, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "coercion is only run the first time x is retrieved, and validation is not run" do + expect(Namer.current_index).to eq 0 + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + end + end + + context "validation and coercion of defaults" do + with_property ':x, String, coerce: proc { |v| "#{v}#{next_index}" }, default: 10' do + it "when x is retrieved, it is coerced before validating and passes" do + expect(resource.x).to eq '101' + end + end + with_property ':x, Integer, coerce: proc { |v| "#{v}#{next_index}" }, default: 10' do + it "when x is retrieved, it is coerced and not validated" do + expect(resource.x).to eq '101' + end + # it "when x is retrieved, it is coerced before validating and fails" do + # expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + # end + end + with_property ':x, String, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "when x is retrieved, it is coerced before validating and passes" do + expect(resource.x).to eq '101' + end + end + with_property ':x, Integer, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "when x is retrieved, it is coerced and not validated" do + expect(resource.x).to eq '101' + end + # it "when x is retrieved, it is coerced before validating and fails" do + # expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + # end + end + with_property ':x, proc { |v| Namer.next_index; true }, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "coercion is only run the first time x is retrieved, and validation is not run" do + expect(Namer.current_index).to eq 0 + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + end + end + end + end + end + + context "Chef::Resource#lazy" do + with_property ':x' do + it "setting x to a lazy value does not run it immediately" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + end + it "you can set x to a lazy value in the instance" do + resource.instance_eval do + x lazy { Namer.next_index } + end + expect(resource.x).to eq 1 + expect(Namer.current_index).to eq 1 + end + it "retrieving a lazy value pops it open" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq 1 + expect(Namer.current_index).to eq 1 + end + it "retrieving a lazy value twice evaluates it twice" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq 1 + expect(resource.x).to eq 2 + expect(Namer.current_index).to eq 2 + end + it "setting the same lazy value on two different instances runs it on each instancee" do + resource2 = resource_class.new("blah2") + l = lazy { Namer.next_index } + resource.x l + resource2.x l + expect(resource2.x).to eq 1 + expect(resource.x).to eq 2 + expect(resource2.x).to eq 3 + end + + context "when the class has a class and instance method named blah" do + before do + resource_class.class_eval do + def self.blah + "class" + end + def blah + "#{name}#{Namer.next_index}" + end + end + end + def blah + "example" + end + # it "retrieving lazy { blah } gets the instance variable" do + # resource.x lazy { blah } + # expect(resource.x).to eq "blah1" + # end + # it "retrieving lazy { blah } from two different instances gets two different instance variables" do + # resource2 = resource_class.new("another") + # l = lazy { blah } + # resource2.x l + # resource.x l + # expect(resource2.x).to eq "another1" + # expect(resource.x).to eq "blah2" + # expect(resource2.x).to eq "another3" + # end + it 'retrieving lazy { |x| "#{blah}#{x.blah}" } gets the example and instance variables' do + resource.x lazy { |x| "#{blah}#{x.blah}" } + expect(resource.x).to eq "exampleblah1" + end + it 'retrieving lazy { |x| "#{blah}#{x.blah}" } from two different instances gets two different instance variables' do + resource2 = resource_class.new("another") + l = lazy { |x| "#{blah}#{x.blah}" } + resource2.x l + resource.x l + expect(resource2.x).to eq "exampleanother1" + expect(resource.x).to eq "exampleblah2" + expect(resource2.x).to eq "exampleanother3" + end + end + end + + with_property ':x, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do + it "lazy values are not coerced on set" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + end + it "lazy values are coerced on get" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 2 + end + it "lazy values are coerced on each access" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 2 + expect(resource.x).to eq "34" + expect(Namer.current_index).to eq 4 + end + end + + with_property ':x, String' do + it "lazy values are not validated on set" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + end + it "lazy values are validated on get" do + resource.x lazy { Namer.next_index } + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + expect(Namer.current_index).to eq 1 + end + end + + with_property ':x, is: proc { |v| Namer.next_index; true }' do + it "lazy values are validated on each access" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq 1 + expect(Namer.current_index).to eq 2 + expect(resource.x).to eq 3 + expect(Namer.current_index).to eq 4 + end + end + + with_property ':x, Integer, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do + it "lazy values are not validated or coerced on set" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + end + it "lazy values are coerced before being validated, which fails" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + expect(Namer.current_index).to eq 2 + end + end + + with_property ':x, coerce: proc { |v| "#{v}#{Namer.next_index}" }, is: proc { |v| Namer.next_index; true }' do + it "lazy values are coerced and validated exactly once" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 3 + expect(resource.x).to eq "45" + expect(Namer.current_index).to eq 6 + end + end + + with_property ':x, String, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do + it "lazy values are coerced before being validated, which succeeds" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 2 + end + end + end + + context "Chef::Resource::PropertyType#coerce" do + with_property ':x, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do + it "coercion runs on set" do + expect(resource.x 10).to eq "101" + expect(Namer.current_index).to eq 1 + end + it "coercion sets the value (and coercion does not run on get)" do + expect(resource.x 10).to eq "101" + expect(resource.x).to eq "101" + expect(Namer.current_index).to eq 1 + end + it "coercion runs each time set happens" do + expect(resource.x 10).to eq "101" + expect(Namer.current_index).to eq 1 + expect(resource.x 10).to eq "102" + expect(Namer.current_index).to eq 2 + end + end + with_property ':x, coerce: proc { |x| Namer.next_index; raise "hi" if x == 10; x }, is: proc { |x| Namer.next_index; x != 10 }' do + it "failed coercion fails to set the value" do + resource.x 20 + expect(resource.x).to eq 20 + expect(Namer.current_index).to eq 2 + expect { resource.x 10 }.to raise_error 'hi' + expect(resource.x).to eq 20 + expect(Namer.current_index).to eq 3 + end + it "validation does not run if coercion fails" do + expect { resource.x 10 }.to raise_error 'hi' + expect(Namer.current_index).to eq 1 + end + end + end + + context "Chef::Resource::PropertyType validation" do + with_property ':x, is: proc { |v| Namer.next_index; v.is_a?(Integer) }' do + it "validation runs on set" do + expect(resource.x 10).to eq 10 + expect(Namer.current_index).to eq 1 + end + it "validation sets the value (and validation does not run on get)" do + expect(resource.x 10).to eq 10 + expect(resource.x).to eq 10 + expect(Namer.current_index).to eq 1 + end + it "validation runs each time set happens" do + expect(resource.x 10).to eq 10 + expect(Namer.current_index).to eq 1 + expect(resource.x 10).to eq 10 + expect(Namer.current_index).to eq 2 + end + it "failed validation fails to set the value" do + expect(resource.x 10).to eq 10 + expect(Namer.current_index).to eq 1 + expect { resource.x 'blah' }.to raise_error Chef::Exceptions::ValidationFailed + expect(resource.x).to eq 10 + expect(Namer.current_index).to eq 2 + end + end + end + + [ 'name_attribute', 'name_property' ].each do |name| + context "Chef::Resource::PropertyType##{name}" do + with_property ":x, #{name}: true" do + it "defaults x to resource.name" do + expect(resource.x).to eq 'blah' + end + it "does not pick up resource.name if set" do + expect(resource.x 10).to eq 10 + expect(resource.x).to eq 10 + end + it "binds to the latest resource.name when run" do + resource.name 'foo' + expect(resource.x).to eq 'foo' + end + it "caches resource.name" do + expect(resource.x).to eq 'blah' + resource.name 'foo' + expect(resource.x).to eq 'blah' + end + end + with_property ":x, default: 10, #{name}: true" do + it "chooses default over #{name}" do + expect(resource.x).to eq 10 + end + end + with_property ":x, #{name}: true, default: 10" do + it "chooses default over #{name}" do + expect(resource.x).to eq 10 + end + end + end + end +end diff --git a/spec/unit/provider_spec.rb b/spec/unit/provider_spec.rb index d7a34bc21b..97b88b1732 100644 --- a/spec/unit/provider_spec.rb +++ b/spec/unit/provider_spec.rb @@ -114,9 +114,7 @@ describe Chef::Provider do end it "does not re-load recipes when creating the temporary run context" do - # we actually want to test that RunContext#load is never called, but we - # can't stub all instances of an object with rspec's mocks. :/ - allow(Chef::RunContext).to receive(:new).and_raise("not supposed to happen") + expect_any_instance_of(Chef::RunContext).not_to receive(:load) snitch = Proc.new {temporary_collection = @run_context.resource_collection} @provider.send(:recipe_eval, &snitch) end diff --git a/spec/unit/run_context/child_run_context_spec.rb b/spec/unit/run_context/child_run_context_spec.rb new file mode 100644 index 0000000000..63586e459c --- /dev/null +++ b/spec/unit/run_context/child_run_context_spec.rb @@ -0,0 +1,133 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Tim Hinderliter (<tim@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Copyright:: Copyright (c) 2008, 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' +require 'support/lib/library_load_order' + +describe Chef::RunContext::ChildRunContext do + context "with a run context with stuff in it" do + let(:chef_repo_path) { File.expand_path(File.join(CHEF_SPEC_DATA, "run_context", "cookbooks")) } + let(:cookbook_collection) { + cl = Chef::CookbookLoader.new(chef_repo_path) + cl.load_cookbooks + Chef::CookbookCollection.new(cl) + } + let(:node) { + node = Chef::Node.new + node.run_list << "test" << "test::one" << "test::two" + node + } + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, cookbook_collection, events) } + + context "and a child run context" do + let(:child) { run_context.create_child } + + it "parent_run_context is set to the parent" do + expect(child.parent_run_context).to eq run_context + end + + it "audits is not the same as the parent" do + expect(child.audits.object_id).not_to eq run_context.audits.object_id + child.audits['hi'] = 'lo' + expect(child.audits['hi']).to eq('lo') + expect(run_context.audits['hi']).not_to eq('lo') + end + + it "resource_collection is not the same as the parent" do + expect(child.resource_collection.object_id).not_to eq run_context.resource_collection.object_id + f = Chef::Resource::File.new('hi', child) + child.resource_collection.insert(f) + expect(child.resource_collection).to include f + expect(run_context.resource_collection).not_to include f + end + + it "immediate_notification_collection is not the same as the parent" do + expect(child.immediate_notification_collection.object_id).not_to eq run_context.immediate_notification_collection.object_id + src = Chef::Resource::File.new('hi', child) + dest = Chef::Resource::File.new('argh', child) + notification = Chef::Resource::Notification.new(dest, :create, src) + child.notifies_immediately(notification) + expect(child.immediate_notification_collection['file[hi]']).to eq([notification]) + expect(run_context.immediate_notification_collection['file[hi]']).not_to eq([notification]) + end + + it "immediate_notifications is not the same as the parent" do + src = Chef::Resource::File.new('hi', child) + dest = Chef::Resource::File.new('argh', child) + notification = Chef::Resource::Notification.new(dest, :create, src) + child.notifies_immediately(notification) + expect(child.immediate_notifications(src)).to eq([notification]) + expect(run_context.immediate_notifications(src)).not_to eq([notification]) + end + + it "delayed_notification_collection is not the same as the parent" do + expect(child.delayed_notification_collection.object_id).not_to eq run_context.delayed_notification_collection.object_id + src = Chef::Resource::File.new('hi', child) + dest = Chef::Resource::File.new('argh', child) + notification = Chef::Resource::Notification.new(dest, :create, src) + child.notifies_delayed(notification) + expect(child.delayed_notification_collection['file[hi]']).to eq([notification]) + expect(run_context.delayed_notification_collection['file[hi]']).not_to eq([notification]) + end + + it "delayed_notifications is not the same as the parent" do + src = Chef::Resource::File.new('hi', child) + dest = Chef::Resource::File.new('argh', child) + notification = Chef::Resource::Notification.new(dest, :create, src) + child.notifies_delayed(notification) + expect(child.delayed_notifications(src)).to eq([notification]) + expect(run_context.delayed_notifications(src)).not_to eq([notification]) + end + + it "create_child creates a child-of-child" do + c = child.create_child + expect(c.parent_run_context).to eq child + end + + context "after load('include::default')" do + before do + run_list = Chef::RunList.new('include::default').expand('_default') + # TODO not sure why we had to do this to get everything to work ... + node.automatic_attrs[:recipes] = [] + child.load(run_list) + end + + it "load_recipe loads into the child" do + expect(child.resource_collection).to be_empty + child.load_recipe("include::includee") + expect(child.resource_collection).not_to be_empty + end + + it "include_recipe loads into the child" do + expect(child.resource_collection).to be_empty + child.include_recipe("include::includee") + expect(child.resource_collection).not_to be_empty + end + + it "load_recipe_file loads into the child" do + expect(child.resource_collection).to be_empty + child.load_recipe_file(File.expand_path("include/recipes/includee.rb", chef_repo_path)) + expect(child.resource_collection).not_to be_empty + end + end + end + end +end diff --git a/spec/unit/run_context_spec.rb b/spec/unit/run_context_spec.rb index e20ba63b72..99801575ef 100644 --- a/spec/unit/run_context_spec.rb +++ b/spec/unit/run_context_spec.rb @@ -68,6 +68,9 @@ describe Chef::RunContext do "dependency2" => { "version" => "0.0.0", }, + "include" => { + "version" => "0.0.0", + }, "no-default-attr" => { "version" => "0.0.0", }, @@ -84,6 +87,10 @@ describe Chef::RunContext do ) end + it "has a nil parent_run_context" do + expect(run_context.parent_run_context).to be_nil + end + describe "loading cookbooks for a run list" do before do |