Permalink
Switch branches/tags
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
646 lines (560 sloc) 19.8 KB
#--
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: AJ Christensen (<aj@chef.io>)
# 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
[: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].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)
immutablize(merge_overrides(path))
end
def combined_default(*path)
immutablize(merge_defaults(path))
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 == NIL ? nil : 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 == NIL ? nil : 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::VividMash.new(merge_with)
elsif merge_with.kind_of?(Array)
Chef::Node::AttrArray.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