#-- # Author:: Adam Jacob () # Author:: AJ Christensen () # Copyright:: Copyright 2008-2018, Chef Software 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 "chef/node/mixin/deep_merge_cache" require "chef/node/mixin/immutablize_hash" require "chef/node/mixin/state_tracking" require "chef/node/immutable_collections" require "chef/node/attribute_collections" require "chef/decorator/unchain" require "chef/mixin/deep_merge" require "chef/log" class Chef class Node # == Attribute # Attribute implements a nested key-value (Hash) and flat collection # (Array) data structure supporting multiple levels of precedence, such # that a given key may have multiple values internally, but will only # return the highest precedence value when reading. class Attribute < Mash include Immutablize # FIXME: what is include Enumerable doing up here, when down below we delegate # most of the Enumerable/Hash things to the underlying merged ImmutableHash. That # is, in fact, the correct, thing to do, while including Enumerable to try to create # a hash-like API gets lots of things wrong because of the difference between the # Hash `each do |key, value|` vs the Array-like `each do |value|` API that Enumerable # expects. This include should probably be deleted? include Enumerable include Chef::Node::Mixin::DeepMergeCache include Chef::Node::Mixin::StateTracking include Chef::Node::Mixin::ImmutablizeHash # List of the component attribute hashes, in order of precedence, low to # high. COMPONENTS = [ :@default, :@env_default, :@role_default, :@force_default, :@normal, :@override, :@role_override, :@env_override, :@force_override, :@automatic, ].freeze DEFAULT_COMPONENTS = [ :@default, :@env_default, :@role_default, :@force_default, ].freeze OVERRIDE_COMPONENTS = [ :@override, :@role_override, :@env_override, :@force_override, ].freeze ENUM_METHODS = [ :all?, :any?, :assoc, :chunk, :collect, :collect_concat, :compare_by_identity, :compare_by_identity?, :count, :cycle, :detect, :drop, :drop_while, :each, :each_cons, :each_entry, :each_key, :each_pair, :each_slice, :each_value, :each_with_index, :each_with_object, :empty?, :entries, :except, :fetch, :find, :find_all, :find_index, :first, :flat_map, :flatten, :grep, :group_by, :has_value?, :include?, :index, :inject, :invert, :key, :keys, :length, :map, :max, :max_by, :merge, :min, :min_by, :minmax, :minmax_by, :none?, :one?, :partition, :rassoc, :reduce, :reject, :reverse_each, :select, :size, :slice_before, :sort, :sort_by, :store, :symbolize_keys, :take, :take_while, :to_a, :to_h, :to_hash, :to_set, :value?, :values, :values_at, :zip, ].freeze ENUM_METHODS.each do |delegated_method| define_method(delegated_method) do |*args, &block| merged_attributes.send(delegated_method, *args, &block) end end # return the cookbook level default attribute component attr_reader :default # return the role level default attribute component attr_reader :role_default # return the environment level default attribute component attr_reader :env_default # return the force_default level attribute component attr_reader :force_default # return the "normal" level attribute component attr_reader :normal # return the cookbook level override attribute component attr_reader :override # return the role level override attribute component attr_reader :role_override # return the enviroment level override attribute component attr_reader :env_override # return the force override level attribute component attr_reader :force_override # return the automatic level attribute component attr_reader :automatic def initialize(normal, default, override, automatic, node = nil) @default = VividMash.new(default, self, node, :default) @env_default = VividMash.new({}, self, node, :env_default) @role_default = VividMash.new({}, self, node, :role_default) @force_default = VividMash.new({}, self, node, :force_default) @normal = VividMash.new(normal, self, node, :normal) @override = VividMash.new(override, self, node, :override) @role_override = VividMash.new({}, self, node, :role_override) @env_override = VividMash.new({}, self, node, :env_override) @force_override = VividMash.new({}, self, node, :force_override) @automatic = VividMash.new(automatic, self, node, :automatic) super(nil, self, node, :merged) end # Debug what's going on with an attribute. +args+ is a path spec to the # attribute you're interested in. For example, to debug where the value # of `node[:network][:default_interface]` is coming from, use: # debug_value(:network, :default_interface). # The return value is an Array of Arrays. The Arrays # are pairs of `["precedence_level", value]`, where precedence level is # the component, such as role default, normal, etc. and value is the # attribute value set at that precedence level. If there is no value at # that precedence level, +value+ will be the symbol +:not_present+. def debug_value(*args) COMPONENTS.map do |component| value = begin instance_variable_get(component).read!(*args) rescue :not_present end [component.to_s.sub(/^@/, ""), value] end end # Set the cookbook level default attribute component to +new_data+. def default=(new_data) reset @default = VividMash.new(new_data, self, __node__, :default) end # Set the role level default attribute component to +new_data+ def role_default=(new_data) reset @role_default = VividMash.new(new_data, self, __node__, :role_default) end # Set the environment level default attribute component to +new_data+ def env_default=(new_data) reset @env_default = VividMash.new(new_data, self, __node__, :env_default) end # Set the force_default (+default!+) level attributes to +new_data+ def force_default=(new_data) reset @force_default = VividMash.new(new_data, self, __node__, :force_default) end # Set the normal level attribute component to +new_data+ def normal=(new_data) reset @normal = VividMash.new(new_data, self, __node__, :normal) end # Set the cookbook level override attribute component to +new_data+ def override=(new_data) reset @override = VividMash.new(new_data, self, __node__, :override) end # Set the role level override attribute component to +new_data+ def role_override=(new_data) reset @role_override = VividMash.new(new_data, self, __node__, :role_override) end # Set the environment level override attribute component to +new_data+ def env_override=(new_data) reset @env_override = VividMash.new(new_data, self, __node__, :env_override) end def force_override=(new_data) reset @force_override = VividMash.new(new_data, self, __node__, :force_override) end def automatic=(new_data) reset @automatic = VividMash.new(new_data, self, __node__, :automatic) end # # Deleting attributes # # clears attributes from all precedence levels def rm(*args) with_deep_merged_return_value(self, *args) do rm_default(*args) rm_normal(*args) rm_override(*args) end end # clears attributes from all default precedence levels # # similar to: force_default!['foo']['bar'].delete('baz') # - does not autovivify # - does not trainwreck if interior keys do not exist def rm_default(*args) with_deep_merged_return_value(combined_default, *args) do default.unlink(*args) role_default.unlink(*args) env_default.unlink(*args) force_default.unlink(*args) end end # clears attributes from normal precedence # # equivalent to: normal!['foo']['bar'].delete('baz') # - does not autovivify # - does not trainwreck if interior keys do not exist def rm_normal(*args) normal.unlink(*args) end # clears attributes from all override precedence levels # # equivalent to: force_override!['foo']['bar'].delete('baz') # - does not autovivify # - does not trainwreck if interior keys do not exist def rm_override(*args) with_deep_merged_return_value(combined_override, *args) do override.unlink(*args) role_override.unlink(*args) env_override.unlink(*args) force_override.unlink(*args) end end def with_deep_merged_return_value(obj, *path, last) hash = obj.read(*path) return nil unless hash.is_a?(Hash) ret = hash[last] yield ret end private :with_deep_merged_return_value # # Replacing attributes without merging # # sets default attributes without merging # # - this API autovivifies (and cannot trainwreck) def default!(*args) return Decorator::Unchain.new(self, :default!) unless args.length > 0 write(:default, *args) end # sets normal attributes without merging # # - this API autovivifies (and cannot trainwreck) def normal!(*args) return Decorator::Unchain.new(self, :normal!) unless args.length > 0 write(:normal, *args) end # sets override attributes without merging # # - this API autovivifies (and cannot trainwreck) def override!(*args) return Decorator::Unchain.new(self, :override!) unless args.length > 0 write(:override, *args) end # clears from all default precedence levels and then sets force_default # # - this API autovivifies (and cannot trainwreck) def force_default!(*args) return Decorator::Unchain.new(self, :force_default!) unless args.length > 0 value = args.pop rm_default(*args) write(:force_default, *args, value) end # clears from all override precedence levels and then sets force_override def force_override!(*args) return Decorator::Unchain.new(self, :force_override!) unless args.length > 0 value = args.pop rm_override(*args) write(:force_override, *args, value) end # # Accessing merged attributes. # # Note that merged_attributes('foo', 'bar', 'baz') can be called to compute only the # deep merge of node['foo']['bar']['baz'], but in practice we currently always compute # all of node['foo'] even if the user only requires node['foo']['bar']['baz']. # def merged_attributes(*path) merge_all(path) end def combined_override(*path) ret = merge_overrides(path) ret == NIL ? nil : ret end def combined_default(*path) ret = merge_defaults(path) ret == NIL ? nil : ret end def normal_unless(*args) return Decorator::Unchain.new(self, :normal_unless) unless args.length > 0 write(:normal, *args) if normal.read(*args[0...-1]).nil? end def default_unless(*args) return Decorator::Unchain.new(self, :default_unless) unless args.length > 0 write(:default, *args) if default.read(*args[0...-1]).nil? end def override_unless(*args) return Decorator::Unchain.new(self, :override_unless) unless args.length > 0 write(:override, *args) if override.read(*args[0...-1]).nil? end def has_key?(key) COMPONENTS.any? do |component_ivar| instance_variable_get(component_ivar).key?(key) end end # method-style access to attributes (has to come after the prepended ImmutablizeHash) def read(*path) merged_attributes.read(*path) end def read!(*path) merged_attributes.read!(*path) end def exist?(*path) merged_attributes.exist?(*path) end def write(level, *args, &block) send(level).write(*args, &block) end def write!(level, *args, &block) send(level).write!(*args, &block) end def unlink(level, *path) send(level).unlink(*path) end def unlink!(level, *path) send(level).unlink!(*path) end alias :attribute? :has_key? alias :member? :has_key? alias :include? :has_key? alias :key? :has_key? alias :each_attribute :each def to_s merged_attributes.to_s end def inspect "#<#{self.class} " << (COMPONENTS + [:@merged_attributes, :@properties]).map do |iv| "#{iv}=#{instance_variable_get(iv).inspect}" end.join(", ") << ">" end private # Helper method for merge_all/merge_defaults/merge_overrides. # # apply_path(thing, [ "foo", "bar", "baz" ]) = thing["foo"]["bar"]["baz"] # # The path value can be nil in which case the whole component is returned. # # It returns nil (does not raise an exception) if it walks off the end of an Mash/Hash/Array, it does not # raise any TypeError if it attempts to apply a hash key to an Integer/String/TrueClass, and just returns # nil in that case. # def apply_path(component, path) path ||= [] path.inject(component) do |val, path_arg| if val.respond_to?(:[]) # Have an Array-like or Hash-like thing if !val.respond_to?(:has_key?) # Have an Array-like thing val[path_arg] elsif val.key?(path_arg) # Hash-like thing (must check has_key? first to protect against Autovivification) val[path_arg] else NIL end else NIL end end end # For elements like Fixnums, true, nil... def safe_dup(e) e.dup rescue TypeError e end # Deep merge all attribute levels using hash-only merging between different precidence # levels (so override arrays completely replace arrays set at any default level). # # The path allows for selectively deep-merging a subtree of the node object. # # @param path [Array] Array of args to method chain to descend into the node object # @return [attr] Deep Merged values (may be VividMash, Hash, Array, etc) from the node object def merge_all(path) components = [ merge_defaults(path), apply_path(@normal, path), merge_overrides(path), apply_path(@automatic, path), ] ret = components.inject(NIL) do |merged, component| hash_only_merge!(merged, component) end ret == NIL ? nil : ret end # Deep merge the default attribute levels with array merging. # # The path allows for selectively deep-merging a subtree of the node object. # # @param path [Array] Array of args to method chain to descend into the node object # @return [attr] Deep Merged values (may be VividMash, Hash, Array, etc) from the node object def merge_defaults(path) ret = DEFAULT_COMPONENTS.inject(NIL) do |merged, component_ivar| component_value = apply_path(instance_variable_get(component_ivar), path) deep_merge!(merged, component_value) end ret end # Deep merge the override attribute levels with array merging. # # The path allows for selectively deep-merging a subtree of the node object. # # @param path [Array] Array of args to method chain to descend into the node object # @return [attr] Deep Merged values (may be VividMash, Hash, Array, etc) from the node object def merge_overrides(path) ret = OVERRIDE_COMPONENTS.inject(NIL) do |merged, component_ivar| component_value = apply_path(instance_variable_get(component_ivar), path) deep_merge!(merged, component_value) end ret end # needed for __path__ def convert_key(key) key.kind_of?(Symbol) ? key.to_s : key end NIL = Object.new # @api private def deep_merge!(merge_onto, merge_with) # If there are two Hashes, recursively merge. if merge_onto.kind_of?(Hash) && merge_with.kind_of?(Hash) merge_with.each do |key, merge_with_value| value = if merge_onto.key?(key) deep_merge!(safe_dup(merge_onto[key]), merge_with_value) else merge_with_value end # internal_set bypasses converting keys, does convert values and allows writing to immutable mashes merge_onto.internal_set(key, value) end merge_onto elsif merge_onto.kind_of?(Array) && merge_with.kind_of?(Array) merge_onto |= merge_with # If merge_with is NIL, don't replace merge_onto elsif merge_with == NIL merge_onto # In all other cases, replace merge_onto with merge_with else if merge_with.kind_of?(Hash) Chef::Node::ImmutableMash.new(merge_with) elsif merge_with.kind_of?(Array) Chef::Node::ImmutableArray.new(merge_with) else merge_with end end end # @api private def hash_only_merge!(merge_onto, merge_with) # If there are two Hashes, recursively merge. if merge_onto.kind_of?(Hash) && merge_with.kind_of?(Hash) 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) else merge_with_value end # internal_set bypasses converting keys, does convert values and allows writing to immutable mashes merge_onto.internal_set(key, value) end merge_onto # If merge_with is NIL, don't replace merge_onto elsif merge_with == NIL merge_onto # In all other cases, replace merge_onto with merge_with else if merge_with.kind_of?(Hash) Chef::Node::ImmutableMash.new(merge_with) elsif merge_with.kind_of?(Array) Chef::Node::ImmutableArray.new(merge_with) else merge_with end end end end end end