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]