summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Smith <tsmith@chef.io>2021-01-15 13:13:09 -0800
committerGitHub <noreply@github.com>2021-01-15 13:13:09 -0800
commit7efa1780adddd87dae1a5b550959de618b27492f (patch)
treef6c2fe90842800bb8d3f6712e86106d1df4b72df
parent415e7fbbd5a4f6992e8941542effb3ba4800f36d (diff)
parent3907a91627ba2850a17a0b4ad5401b0bc3a5d219 (diff)
downloadchef-7efa1780adddd87dae1a5b550959de618b27492f.tar.gz
Merge pull request #10861 from chef/lcg/lazy-attributes
Signed-off-by: Tim Smith <tsmith@chef.io>
-rw-r--r--chef-utils/lib/chef-utils/mash.rb15
-rw-r--r--lib/chef/delayed_evaluator.rb4
-rw-r--r--lib/chef/node/attribute.rb4
-rw-r--r--lib/chef/node/immutable_collections.rb13
-rw-r--r--lib/chef/node/mixin/deep_merge_cache.rb18
-rw-r--r--spec/unit/node_spec.rb78
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