diff options
-rw-r--r-- | chef-utils/lib/chef-utils/mash.rb | 15 | ||||
-rw-r--r-- | lib/chef/delayed_evaluator.rb | 4 | ||||
-rw-r--r-- | lib/chef/node/attribute.rb | 4 | ||||
-rw-r--r-- | lib/chef/node/immutable_collections.rb | 13 | ||||
-rw-r--r-- | lib/chef/node/mixin/deep_merge_cache.rb | 18 | ||||
-rw-r--r-- | spec/unit/node_spec.rb | 78 |
6 files changed, 123 insertions, 9 deletions
diff --git a/chef-utils/lib/chef-utils/mash.rb b/chef-utils/lib/chef-utils/mash.rb index 484e172b33..bb48064aa3 100644 --- a/chef-utils/lib/chef-utils/mash.rb +++ b/chef-utils/lib/chef-utils/mash.rb @@ -94,6 +94,10 @@ module ChefUtils end end + unless method_defined?(:regular_reader) + alias_method :regular_reader, :[] + end + unless method_defined?(:regular_writer) alias_method :regular_writer, :[]= end @@ -102,6 +106,11 @@ module ChefUtils alias_method :regular_update, :update end + # @param key<Object> The key to get. + def [](key) + regular_reader(key) + end + # @param key<Object> The key to set. # @param value<Object> # The value to set the key to. @@ -114,6 +123,12 @@ module ChefUtils # internal API for use by Chef's deep merge cache # @api private + def internal_get(key) + regular_reader(key) + end + + # internal API for use by Chef's deep merge cache + # @api private def internal_set(key, value) regular_writer(key, convert_value(value)) end diff --git a/lib/chef/delayed_evaluator.rb b/lib/chef/delayed_evaluator.rb index df734c8303..f200f88410 100644 --- a/lib/chef/delayed_evaluator.rb +++ b/lib/chef/delayed_evaluator.rb @@ -17,5 +17,9 @@ class Chef class DelayedEvaluator < Proc + def dup + # super returns a "Proc" (which seems buggy) so re-wrap it + self.class.new(&super) # rubocop:disable Layout/SpaceAroundKeyword + end end end diff --git a/lib/chef/node/attribute.rb b/lib/chef/node/attribute.rb index 29b60a98d5..3383b3c7e5 100644 --- a/lib/chef/node/attribute.rb +++ b/lib/chef/node/attribute.rb @@ -596,7 +596,7 @@ class Chef merge_with.each do |key, merge_with_value| value = if merge_onto.key?(key) - deep_merge!(safe_dup(merge_onto[key]), merge_with_value) + deep_merge!(safe_dup(merge_onto.internal_get(key)), merge_with_value) else merge_with_value end @@ -632,7 +632,7 @@ class Chef merge_with.each do |key, merge_with_value| value = if merge_onto.key?(key) - hash_only_merge!(safe_dup(merge_onto[key]), merge_with_value) + hash_only_merge!(safe_dup(merge_onto.internal_get(key)), merge_with_value) else merge_with_value end diff --git a/lib/chef/node/immutable_collections.rb b/lib/chef/node/immutable_collections.rb index 49dc0256b9..79b1983a05 100644 --- a/lib/chef/node/immutable_collections.rb +++ b/lib/chef/node/immutable_collections.rb @@ -19,6 +19,7 @@ require_relative "common_api" require_relative "mixin/state_tracking" require_relative "mixin/immutablize_array" require_relative "mixin/immutablize_hash" +require_relative "../delayed_evaluator" class Chef class Node @@ -109,6 +110,12 @@ class Chef key end + def [](*args) + value = super + value = value.call while value.is_a?(::Chef::DelayedEvaluator) + value + end + prepend Chef::Node::Mixin::StateTracking prepend Chef::Node::Mixin::ImmutablizeArray end @@ -187,6 +194,12 @@ class Chef e end + def [](*args) + value = super + value = value.call while value.is_a?(::Chef::DelayedEvaluator) + value + end + prepend Chef::Node::Mixin::StateTracking prepend Chef::Node::Mixin::ImmutablizeHash end diff --git a/lib/chef/node/mixin/deep_merge_cache.rb b/lib/chef/node/mixin/deep_merge_cache.rb index e88e079895..8978d77ea0 100644 --- a/lib/chef/node/mixin/deep_merge_cache.rb +++ b/lib/chef/node/mixin/deep_merge_cache.rb @@ -15,6 +15,8 @@ # limitations under the License. # +require_relative "../../delayed_evaluator" + class Chef class Node module Mixin @@ -46,13 +48,15 @@ class Chef alias :reset :reset_cache def [](key) - if deep_merge_cache.key?(key.to_s) - # return the cache of the deep merged values by top-level key - deep_merge_cache[key.to_s] - else - # save all the work of computing node[key] - deep_merge_cache[key.to_s] = merged_attributes(key) - end + ret = if deep_merge_cache.key?(key.to_s) + # return the cache of the deep merged values by top-level key + deep_merge_cache[key.to_s] + else + # save all the work of computing node[key] + deep_merge_cache[key.to_s] = merged_attributes(key) + end + ret = ret.call while ret.is_a?(::Chef::DelayedEvaluator) + ret end end diff --git a/spec/unit/node_spec.rb b/spec/unit/node_spec.rb index ec7beb9a50..284a993f5c 100644 --- a/spec/unit/node_spec.rb +++ b/spec/unit/node_spec.rb @@ -1951,4 +1951,82 @@ describe Chef::Node do expect(node["foo"]["bar"]["test"]).to eql("right") end end + + describe "lazy values" do + it "supports lazy values in attributes" do + node.instance_eval do + default["foo"]["bar"] = lazy { node["fizz"]["buzz"] } + default["fizz"]["buzz"] = "works" + end + expect(node["foo"]["bar"]).to eql("works") + end + + it "lazy values maintain laziness" do + node.instance_eval do + default["foo"]["bar"] = lazy { node["fizz"]["buzz"] } + default["fizz"]["buzz"] = "works" + end + expect(node["foo"]["bar"]).to eql("works") + node.default["fizz"]["buzz"] = "still works" + expect(node["foo"]["bar"]).to eql("still works") + end + + it "supports recursive lazy values in attributes" do + node.instance_eval do + default["cool"]["beans"] = lazy { node["foo"]["bar"] } + default["foo"]["bar"] = lazy { node["fizz"]["buzz"] } + default["fizz"]["buzz"] = "works" + end + expect(node["cool"]["beans"]).to eql("works") + node.default["fizz"]["buzz"] = "still works" + expect(node["cool"]["beans"]).to eql("still works") + end + + it "supports top level lazy values in attributes" do + # due to the top level deep merge cache these are special cases + node.instance_eval do + default["cool"] = lazy { node["foo"] } + default["foo"] = lazy { node["fizz"] } + default["fizz"] = "works" + end + expect(node["cool"]).to eql("works") + node.default["fizz"] = "still works" + expect(node["cool"]).to eql("still works") + end + + it "supports deep merged values in attributes" do + node.instance_eval do + override["bar"]["cool"] = lazy { node["bar"]["foo"] } + override["bar"]["foo"] = lazy { node["bar"]["fizz"] } + override["bar"]["fizz"] = "works" + end + expect(node["bar"]["cool"]).to eql("works") + node.override["bar"]["fizz"] = "still works" + expect(node["bar"]["cool"]).to eql("still works") + end + + it "supports overridden deep merged values in attributes (deep_merge)" do + node.instance_eval do + role_override["bar"] = { "cool" => "bad", "foo" => "bad", "fizz" => "bad" } + force_override["bar"]["cool"] = lazy { node["bar"]["foo"] } + force_override["bar"]["foo"] = lazy { node["bar"]["fizz"] } + force_override["bar"]["fizz"] = "works" + end + expect(node["bar"]["cool"]).to eql("works") + node.force_override["bar"]["fizz"] = "still works" + expect(node["bar"]["cool"]).to eql("still works") + end + + it "supports overridden deep merged values in attributes (hash_only_merge)" do + node.instance_eval do + default["bar"] = { "cool" => "bad", "foo" => "bad", "fizz" => "bad" } + override["bar"]["cool"] = lazy { node["bar"]["foo"] } + override["bar"]["foo"] = lazy { node["bar"]["fizz"] } + override["bar"]["fizz"] = "works" + end + expect(node["bar"]["cool"]).to eql("works") + node.override["bar"]["fizz"] = "still works" + expect(node["bar"]["cool"]).to eql("still works") + end + end end |