require_relative "../delayed_evaluator" require_relative "params_validate" require_relative "../property" class Chef module Mixin module Properties module ClassMethods # # The list of properties defined on this resource. # # Everything defined with `property` is in this list. # # @param include_superclass [Boolean] `true` to include properties defined # on superclasses; `false` or `nil` to return the list of properties # directly on this class. # # @return [Hash] The list of property names and types. # def properties(include_superclass = true) if include_superclass result = {} ancestors.reverse_each { |c| result.merge!(c.properties(false)) if c.respond_to?(:properties) } result else @properties ||= {} end 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] The type(s) of this property. # If present, this is prepended to the `is` validation option. # @param options [Hash] 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] :regex An object, or # list of objects, that must match the value with `regex.match(value)`. # @option options [Class,Array] :kind_of A class, or # list of classes, that the value must be an instance of. # @option options [Hash] :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] :respond_to A method # name, or list of method names, the value must respond to. # @option options [Symbol,Array] :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). # @option options [String] :description A description of the property. # @option options [String] :introduced The release that introduced this property # @option options [Boolean] :desired_state `true` if this property is # part of desired state. Defaults to `true`. # @option options [Boolean] :identity `true` if this property # is part of object identity. Defaults to `false`. # @option options [Boolean] :sensitive `true` if this property could # contain sensitive information and whose value should be redacted # in any resource reporting output. Defaults to `false`. # # @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 property(name, type = NOT_PASSED, **options) name = name.to_sym options = options.inject({}) { |memo, (key, value)| memo[key.to_sym] = value; memo } options[:instance_variable_name] = :"@#{name}" unless options.key?(:instance_variable_name) options[:name] = name options[:declared_in] = self if type == NOT_PASSED # If a type is not passed, the property derives from the # superclass property (if any) if properties.key?(name) property = properties[name].derive(**options) else property = property_type(**options) end # If a Property is specified, derive a new one from that. elsif type.is_a?(Property) || (type.is_a?(Class) && type <= Property) property = type.derive(**options) # If a primitive type was passed, combine it with "is" else if options[:is] options[:is] = ([ type ] + [ options[:is] ]).flatten(1) else options[:is] = type end property = property_type(**options) end local_properties = properties(false) local_properties[name] = property property.emit_dsl end # # Create a reusable property type that can be used in multiple properties # in different resources. # # @param options [Hash] Validation options. see #property for # the list of options. # # @example # property_type(default: 'hi') # def property_type(**options) Property.derive(**options) end def deprecated_property_alias(from, to, message) Property.emit_deprecated_alias(from, to, message, self) 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 lazy(&block) DelayedEvaluator.new(&block) end # # Get or set the list of desired state properties for this resource. # # State properties are properties that describe the desired state # of the system, such as file permissions or ownership. # In general, state properties are properties that could be populated by # examining the state of the system (e.g., File.stat can tell you the # permissions on an existing file). Contrarily, properties that are not # "state properties" usually modify the way Chef itself behaves, for example # by providing additional options for a package manager to use when # installing a package. # # This method is unnecessary when declaring properties with `property`; # properties are added to state_properties by default, and can be turned off # with `desired_state: false`. # # ```ruby # property :x # part of desired state # property :y, desired_state: false # not part of desired state # ``` # # @param names [Array] A list of property names to set as desired # state. # # @return [Array] All properties in desired state. # def state_properties(*names) unless names.empty? names = names.map(&:to_sym).uniq local_properties = properties(false) # Add new properties to the list. names.each do |name| property = properties[name] if !property self.property name, instance_variable_name: false, desired_state: true elsif !property.desired_state? self.property name, desired_state: true end end # If state_attrs *excludes* something which is currently desired state, # mark it as desired_state: false. local_properties.each do |name, property| if property.desired_state? && !names.include?(name) self.property name, desired_state: false end end end properties.values.select(&:desired_state?) end # # Set the identity of this resource to a particular set of properties. # # This drives #identity, which returns data that uniquely refers to a given # resource on the given node (in such a way that it can be correlated # across Chef runs). # # This method is unnecessary when declaring properties with `property`; # properties can be added to identity during declaration with # `identity: true`. # # ```ruby # property :x, identity: true # part of identity # property :y # not part of identity # ``` # # If no properties are marked as identity, "name" is considered the identity. # # @param names [Array] A list of property names to set as the identity. # # @return [Array] All identity properties. # def identity_properties(*names) unless names.empty? names = names.map(&:to_sym) # Add or change properties that are not part of the identity. names.each do |name| property = properties[name] if !property self.property name, instance_variable_name: false, identity: true elsif !property.identity? self.property name, identity: true end end # If identity_properties *excludes* something which is currently part of # the identity, mark it as identity: false. properties.each do |name, property| if property.identity? && !names.include?(name) self.property name, identity: false end end end result = properties.values.select(&:identity?) # if there are no other identity properites set, then the name_property becomes the identity, or # failing that we use the actual name. if result.empty? result = name_property ? [ properties[name_property] ] : [ properties[:name] ] end result end # Returns the name of the name property. Returns nil if there is no name property. # # @return [Symbol] the name property for this resource def name_property p = properties.find { |n, p| p.name_property? } p ? p.first : nil end def included(other) other.extend ClassMethods end end def self.included(other) other.extend ClassMethods end include Chef::Mixin::ParamsValidate # # Whether this property has been set (or whether it has a default that has # been retrieved). # # @param name [Symbol] The name of the property. # @return [Boolean] `true` if the property has been set. # def property_is_set?(name) property = self.class.properties[name.to_sym] raise ArgumentError, "Property #{name} is not defined in class #{self}" unless property property.is_set?(self) end # # Clear this property as if it had never been set. It will thereafter return # the default. # been retrieved). # # @param name [Symbol] The name of the property. # def reset_property(name) property = self.class.properties[name.to_sym] raise ArgumentError, "Property #{name} is not defined in class #{self}" unless property property.reset(self) end # # The description of the property # # @param name [Symbol] The name of the property. # @return [String] The description of the property. def property_description(name) property = self.class.properties[name.to_sym] raise ArgumentError, "Property #{name} is not defined in class #{self}" unless property property.description end # Copy properties from another property object (resource) # # By default this copies all properties other than the name property (that is required to create the # destination object so it has already been done in advance and this way we do not clobber the name # that was set in that constructor). By default it copies everything, optional arguments can be use # to only select a subset. Or specific excludes can be set (and the default exclude on the name property # can also be overridden). Exclude has priority over include, although the caller is likely better # off doing the set arithmetic themselves for explicitness. # # action :doit do # # use it inside a block # file "/etc/whatever.xyz" do # copy_properties_from new_resource # end # # # or directly call it # r = declare_resource(:file, "etc/whatever.xyz") # r.copy_properties_from(new_resource, :owner, :group, :mode) # end # # @param other [Object] the other object (Chef::Resource) which implements the properties API # @param includes [Array] splat-args list of symbols of the properties to copy. # @param exclude [Array] list of symbosl of the properties to exclude. # @return the self object the properties were copied to for method chaining # def copy_properties_from(other, *includes, exclude: [ :name ]) includes = other.class.properties.keys if includes.empty? includes -= exclude includes.each do |p| send(p, other.send(p)) if other.property_is_set?(p) end self end end end end