lib/puppet/resource_api.rb in puppet-resource_api-1.2.0 vs lib/puppet/resource_api.rb in puppet-resource_api-1.3.0

- old
+ new

@@ -5,13 +5,21 @@ require 'puppet/resource_api/version' require 'puppet/type' module Puppet::ResourceApi def register_type(definition) - raise Puppet::DevError, 'requires a Hash as definition, not %{other_type}' % { other_type: definition.class } unless definition.is_a? Hash - raise Puppet::DevError, 'requires a name' unless definition.key? :name - raise Puppet::DevError, 'requires attributes' unless definition.key? :attributes + raise Puppet::DevError, 'requires a hash as definition, not `%{other_type}`' % { other_type: definition.class } unless definition.is_a? Hash + raise Puppet::DevError, 'requires a `:name`' unless definition.key? :name + raise Puppet::DevError, 'requires `:attributes`' unless definition.key? :attributes + raise Puppet::DevError, '`:attributes` must be a hash, not `%{other_type}`' % { other_type: definition[:attributes].class } unless definition[:attributes].is_a?(Hash) + [:title, :provider, :alias, :audit, :before, :consume, :export, :loglevel, :noop, :notify, :require, :schedule, :stage, :subscribe, :tag].each do |name| + raise Puppet::DevError, 'must not define an attribute called `%{name}`' % { name: name.inspect } if definition[:attributes].key? name + end + if definition.key?(:title_patterns) && !definition[:title_patterns].is_a?(Array) + raise Puppet::DevError, '`:title_patterns` must be an array, not `%{other_type}`' % { other_type: definition[:title_patterns].class } + end + validate_ensure(definition) definition[:features] ||= [] supported_features = %w[supports_noop canonicalize remote_resource simple_get_filter].freeze unknown_features = definition[:features] - supported_features @@ -35,12 +43,10 @@ Puppet::Provider.const_set(class_name_from_type_name(definition[:name]), Module.new) end Puppet::Type.newtype(definition[:name].to_sym) do @docs = definition[:docs] - has_namevar = false - namevar_name = nil # Keeps a copy of the provider around. Weird naming to avoid clashes with puppet's own `provider` member define_singleton_method(:my_provider) do @my_provider ||= Puppet::ResourceApi.load_provider(definition[:name]).new end @@ -63,26 +69,41 @@ end define_method(:initialize) do |attributes| # $stderr.puts "A: #{attributes.inspect}" if attributes.is_a? Puppet::Resource + @title = attributes.title attributes = attributes.to_hash else - @called_from_resource = true + @ral_find_absent = true end # $stderr.puts "B: #{attributes.inspect}" if type_definition.feature?('canonicalize') attributes = my_provider.canonicalize(context, [attributes])[0] end - # $stderr.puts "C: #{attributes.inspect}" + + # the `Puppet::Resource::Ral.find` method, when `instances` does not return a match, uses a Hash with a `:name` key to create + # an "absent" resource. This is often hit by `puppet resource`. This needs to work, even if the namevar is not called `name`. + # This bit here relies on the default `title_patterns` (see below) to match the title back to the first (and often only) namevar + if definition[:attributes][:name].nil? && attributes[:title].nil? + attributes[:title] = attributes.delete(:name) + if attributes[:title].nil? && !type_definition.namevars.empty? + attributes[:title] = @title + end + end + super(attributes) end validate do # enforce mandatory attributes @missing_attrs = [] @missing_params = [] + + # do not validate on known-absent instances + return if @ral_find_absent + definition[:attributes].each do |name, options| type = Puppet::Pops::Types::TypeParser.singleton.parse(options[:type]) # skip read only vars and the namevar next if [:read_only, :namevar].include? options[:behaviour] @@ -96,11 +117,11 @@ @missing_attrs << name @missing_params << name if options[:behaviour] == :parameter end end - @missing_attrs -= [:ensure] if @called_from_resource + @missing_attrs -= [:ensure] raise_missing_params if @missing_params.any? end definition[:attributes].each do |name, options| @@ -113,11 +134,11 @@ end # TODO: using newparam everywhere would suppress change reporting # that would allow more fine-grained reporting through context, # but require more invest in hooking up the infrastructure to emulate existing data - param_or_property = if [:read_only, :parameter, :namevar].include? options[:behaviour] + param_or_property = if [:read_only, :parameter].include? options[:behaviour] :newparam else :newproperty end @@ -131,15 +152,11 @@ else warn("#{definition[:name]}.#{name} has no docs") end if options[:behaviour] == :namevar - # puts 'setting namevar' - # raise Puppet::DevError, "namevar must be called 'name', not '#{name}'" if name.to_s != 'name' isnamevar - has_namevar = true - namevar_name = name end # read-only values do not need type checking, but can have default values if options[:behaviour] != :read_only if options.key? :default @@ -197,20 +214,20 @@ define_method(:rs_value) do @value end end - if type.instance_of? Puppet::Pops::Types::POptionalType - type = type.type - end - # puppet symbolizes some values through puppet/parameter/value.rb (see .convert()), but (especially) Enums # are strings. specifying a munge block here skips the value_collection fallback in puppet/parameter.rb's # default .unsafe_munge() implementation. munge { |v| v } # provide hints to `puppet type generate` for better parsing + if type.instance_of? Puppet::Pops::Types::POptionalType + type = type.type + end + case type when Puppet::Pops::Types::PStringType # require any string value Puppet::ResourceApi.def_newvalues(self, param_or_property, %r{}) when Puppet::Pops::Types::PBooleanType @@ -248,35 +265,37 @@ my_provider.get(context).map do |resource_hash| resource_hash.each do |key| property = definition[:attributes][key.first] attr_def[key.first] = property end - if resource_hash[namevar_name].nil? - raise Puppet::ResourceError, "`#{name}.get` did not return a value for the `#{namevar_name}` namevar attribute" + context.type.namevars.each do |namevar| + if resource_hash[namevar].nil? + raise Puppet::ResourceError, "`#{name}.get` did not return a value for the `#{namevar}` namevar attribute" + end end - Puppet::ResourceApi::TypeShim.new(resource_hash[namevar_name], resource_hash, name, namevar_name, attr_def) + Puppet::ResourceApi::TypeShim.new(resource_hash, name, context.type.namevars, attr_def) end end define_method(:retrieve) do # puts "retrieve(#{title.inspect})" result = Puppet::Resource.new(self.class, title) current_state = if type_definition.feature?('simple_get_filter') my_provider.get(context, [title]).first else - my_provider.get(context).find { |h| h[namevar_name] == title } + my_provider.get(context).find { |h| namevar_match?(h) } end strict_check(current_state) if current_state && type_definition.feature?('canonicalize') if current_state current_state.each do |k, v| result[k] = v end else - result[namevar_name] = title + result[:title] = title result[:ensure] = :absent if type_definition.ensurable? end # puppet needs ensure to be a symbol result[:ensure] = result[:ensure].to_sym if type_definition.ensurable? && result[:ensure].is_a?(String) @@ -286,16 +305,22 @@ @rapi_current_state = current_state Puppet.debug("Current State: #{@rapi_current_state.inspect}") result end + define_method(:namevar_match?) do |item| + context.type.namevars.all? do |namevar| + item[namevar] == @parameters[namevar].value if @parameters[namevar].respond_to? :value + end + end + define_method(:flush) do raise_missing_attrs # puts 'flush' # skip puppet's injected metaparams - target_state = Hash[@parameters.reject { |k, _v| [:loglevel, :noop].include? k }.map { |k, v| [k, v.rs_value] }] + target_state = Hash[@parameters.reject { |k, _v| [:loglevel, :noop, :provider].include? k }.map { |k, v| [k, v.rs_value] }] target_state = my_provider.canonicalize(context, [target_state]).first if type_definition.feature?('canonicalize') retrieve unless @rapi_current_state return if @rapi_current_state == target_state @@ -346,11 +371,11 @@ return unless state_clone && (current_state != state_clone) #:nocov: # codecov fails to register this multiline as covered, even though simplecov does. message = <<MESSAGE.strip -#{definition[:name]}[#{current_state[namevar_name]}]#get has not provided canonicalized values. +#{definition[:name]}[#{@title}]#get has not provided canonicalized values. Returned values: #{current_state.inspect} Canonicalized values: #{state_clone.inspect} MESSAGE #:nocov: @@ -370,10 +395,35 @@ def context self.class.context end + define_singleton_method(:title_patterns) do + @title_patterns ||= if definition.key? :title_patterns + parse_title_patterns(definition[:title_patterns]) + else + [[%r{(.*)}m, [[type_definition.namevars.first]]]] + end + end + + # Creates a `title_pattern` compatible data structure to pass to the underlying puppet runtime environment. + # It uses the named items in the regular expression to connect the dots + # + # @example `[ %r{^(?<package>.*[^-])-(?<manager>.*)$} ]` becomes + # [ + # [ + # %r{^(?<package>.*[^-])-(?<manager>.*)$}, + # [ [:package], [:manager] ] + # ], + # ] + def self.parse_title_patterns(patterns) + patterns.map do |item| + regex = Regexp.new(item[:pattern]) + [item[:pattern], regex.names.map { |x| [x.to_sym] }] + end + end + [:autorequire, :autobefore, :autosubscribe, :autonotify].each do |auto| next unless definition[auto] definition[auto].each do |type, values| Puppet.debug("Registering #{auto} for #{type}: #{values.inspect}") @@ -418,22 +468,36 @@ this.newvalue(v) {} end end end - # Validates and munges values coming from puppet into a shape palatable to the provider. - # This includes parsing values from strings, e.g. when running in `puppet resource`. + def self.caller_is_resource_app? + caller.any? { |c| c.match(%r{application/resource.rb:}) } + end + + # This method handles translating values from the runtime environment to the expected types for the provider. + # When being called from `puppet resource`, it tries to transform the strings from the command line into their + # expected ruby representations, e.g. `"2"` (a string), will be transformed to `2` (the number) if (and only if) + # the target `type` is `Integer`. + # Additionally this function also validates that the passed in (and optionally transformed) value matches the + # specified type. # @param type[Puppet::Pops::Types::TypedModelObject] the type to check/clean against # @param value the value to clean # @param error_msg_prefix[String] a prefix for the error messages # @return [type] the cleaned value # @raise [Puppet::ResourceError] if `value` could not be parsed into `type` def self.mungify(type, value, error_msg_prefix) - cleaned_value, error = try_mungify(type, value, error_msg_prefix) - - raise Puppet::ResourceError, error if error - + if caller_is_resource_app? + # When the provider is exercised from the `puppet resource` CLI, we need to unpack strings into + # the correct types, e.g. "1" (a string) to 1 (an integer) + cleaned_value, error_msg = try_mungify(type, value, error_msg_prefix) + raise Puppet::ResourceError, error_msg if error_msg + else + # Every other time, we can use the values as is + cleaned_value = value + end + Puppet::ResourceApi.validate(type, cleaned_value, error_msg_prefix) cleaned_value end # Recursive implementation part of #mungify. Uses a multi-valued return value to avoid excessive # exception throwing for regular usage @@ -492,16 +556,44 @@ return try_mungify(string_type, value, error_msg_prefix) if string_type # fall through to default handling end - # a match! - return [value, nil] if type.instance?(value) + error_msg = try_validate(type, value, error_msg_prefix) + if error_msg + # an error :-( + [nil, error_msg] + else + # a match! + [value, nil] + end + end + # Validates the `value` against the specified `type`. + # @param type[Puppet::Pops::Types::TypedModelObject] the type to check against + # @param value the value to clean + # @param error_msg_prefix[String] a prefix for the error messages + # @raise [Puppet::ResourceError] if `value` is not of type `type` + # @private + def self.validate(type, value, error_msg_prefix) + error_msg = try_validate(type, value, error_msg_prefix) + + raise Puppet::ResourceError, error_msg if error_msg + end + + # Tries to validate the `value` against the specified `type`. + # @param type[Puppet::Pops::Types::TypedModelObject] the type to check against + # @param value the value to clean + # @param error_msg_prefix[String] a prefix for the error messages + # @return [String, nil] a error message indicating the problem, or `nil` if the value was valid. + # @private + def self.try_validate(type, value, error_msg_prefix) + return nil if type.instance?(value) + # an error :-( inferred_type = Puppet::Pops::Types::TypeCalculator.infer_set(value) error_msg = Puppet::Pops::Types::TypeMismatchDescriber.new.describe_mismatch(error_msg_prefix, type, inferred_type) - [nil, error_msg] + error_msg end def self.validate_ensure(definition) return unless definition[:attributes].key? :ensure options = definition[:attributes][:ensure]