module Puppet::ResourceApi

This module contains the main API to register and access types, providers and transports.

Provides accessor methods for the type being provided

Constants

VERSION

Attributes

warning_count[RW]

Public Class Methods

build_title(type_definition, resource_hash) click to toggle source
# File lib/puppet/resource_api.rb, line 114
def self.build_title(type_definition, resource_hash)
  if type_definition.namevars.size > 1
    # use a MonkeyHash to allow searching in Puppet's RAL
    Puppet::ResourceApi::MonkeyHash[type_definition.namevars.map { |attr| [attr, resource_hash[attr]] }]
  else
    resource_hash[type_definition.namevars[0]]
  end
end
caller_is_resource_app?() click to toggle source
# File lib/puppet/resource_api.rb, line 535
def self.caller_is_resource_app?
  caller.any? { |c| c.match(%r{application/resource.rb:}) }
end
class_name_from_type_name(type_name) click to toggle source
# File lib/puppet/resource_api.rb, line 531
def self.class_name_from_type_name(type_name)
  type_name.to_s.split('_').map(&:capitalize).join
end
instances() click to toggle source
# File lib/puppet/resource_api.rb, line 228
def self.instances
  # puts 'instances'
  # force autoloading of the provider
  provider(type_definition.name)

  initial_fetch = if type_definition.feature?('simple_get_filter')
                    my_provider.get(context, [])
                  else
                    my_provider.get(context)
                  end

  initial_fetch.map do |resource_hash|
    type_definition.check_schema(resource_hash)
    # allow a :title from the provider to override the default
    result = if resource_hash.key? :title
               new(title: resource_hash[:title])
             else
               new(title: build_title(type_definition, resource_hash))
             end
    result.cache_current_state(resource_hash)
    result
  end
end
load_default_provider(class_name, type_name_sym) click to toggle source
# File lib/puppet/resource_api.rb, line 506
def load_default_provider(class_name, type_name_sym)
  # loads the "puppet/provider/#{type_name}/#{type_name}" file through puppet
  Puppet::Type.type(type_name_sym).provider(type_name_sym)
  Puppet::Provider.const_get(class_name, false).const_get(class_name, false)
end
load_device_provider(class_name, type_name_sym, device_class_name, device_name_sym) click to toggle source
# File lib/puppet/resource_api.rb, line 513
def load_device_provider(class_name, type_name_sym, device_class_name, device_name_sym)
  # loads the "puppet/provider/#{type_name}/#{device_name}" file through puppet
  Puppet::Type.type(type_name_sym).provider(device_name_sym)
  provider_module = Puppet::Provider.const_get(class_name, false)
  if provider_module.const_defined?(device_class_name, false)
    provider_module.const_get(device_class_name, false)
  else
    load_default_provider(class_name, type_name_sym)
  end
end
load_provider(type_name) click to toggle source
# File lib/puppet/resource_api.rb, line 477
def load_provider(type_name)
  class_name = class_name_from_type_name(type_name)
  type_name_sym = type_name.to_sym
  device_name = if Puppet::Util::NetworkDevice.current.nil?
                  nil
                elsif Puppet::Util::NetworkDevice.current.is_a? Puppet::ResourceApi::Transport::Wrapper
                  # extract the device type from the currently loaded device's class
                  Puppet::Util::NetworkDevice.current.schema.name
                else
                  Puppet::Util::NetworkDevice.current.class.name.split('::')[-2].downcase
                end
  device_class_name = class_name_from_type_name(device_name)

  if device_name
    device_name_sym = device_name.to_sym if device_name
    load_device_provider(class_name, type_name_sym, device_class_name, device_name_sym)
  else
    load_default_provider(class_name, type_name_sym)
  end
rescue NameError
  if device_name # line too long # rubocop:disable Style/GuardClause
    raise Puppet::DevError, "Found neither the device-specific provider class Puppet::Provider::#{class_name}::#{device_class_name} in puppet/provider/#{type_name}/#{device_name}"\
    " nor the generic provider class Puppet::Provider::#{class_name}::#{class_name} in puppet/provider/#{type_name}/#{type_name}"
  else
    raise Puppet::DevError, "provider class Puppet::Provider::#{class_name}::#{class_name} not found in puppet/provider/#{type_name}/#{type_name}"
  end
end
new(attributes) click to toggle source
Calls superclass method
# File lib/puppet/resource_api.rb, line 72
def initialize(attributes)
  # $stderr.puts "A: #{attributes.inspect}"
  if attributes.is_a? Puppet::Resource
    @title = attributes.title
    @catalog = attributes.catalog
    sensitives = attributes.sensitive_parameters
    attributes = attributes.to_hash
  else
    @ral_find_absent = true
    sensitives = []
  end

  # undo puppet's unwrapping of Sensitive values to provide a uniform experience for providers
  # See https://tickets.puppetlabs.com/browse/PDK-1091 for investigation and background
  sensitives.each do |name|
    if attributes.key?(name) && !attributes[name].is_a?(Puppet::Pops::Types::PSensitiveType::Sensitive)
      attributes[name] = Puppet::Pops::Types::PSensitiveType::Sensitive.new(attributes[name])
    end
  end

  # $stderr.puts "B: #{attributes.inspect}"
  if type_definition.feature?('canonicalize')
    attributes = my_provider.canonicalize(context, [attributes])[0]
  end

  # 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 type_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
parse_title_patterns(patterns) click to toggle source

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] ]
  ],
]
# File lib/puppet/resource_api.rb, line 446
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
register_transport(schema) click to toggle source

keeps the existing register API format. e.g. Puppet::ResourceApi.register_type

# File lib/puppet/resource_api.rb, line 526
def register_transport(schema)
  Puppet::ResourceApi::Transport.register(schema)
end
register_type(definition) click to toggle source
# File lib/puppet/resource_api.rb, line 26
  def register_type(definition)
    # Attempt to create a TypeDefinition from the input hash
    # This will validate and throw if its not right
    type_def = TypeDefinition.new(definition)

    # prepare the ruby module for the provider
    # this has to happen before Puppet::Type.newtype starts autoloading providers
    # it also needs to be guarded against the namespace already being defined by something
    # else to avoid ruby warnings
    unless Puppet::Provider.const_defined?(class_name_from_type_name(definition[:name]), false)
      Puppet::Provider.const_set(class_name_from_type_name(definition[:name]), Module.new)
    end

    Puppet::Type.newtype(definition[:name].to_sym) do
      # The :desc value is already cleaned up by the TypeDefinition validation
      @doc = definition[:desc]
      @type_definition = type_def

      # 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 ||= Hash.new { |hash, key| hash[key] = Puppet::ResourceApi.load_provider(definition[:name]).new }

        if Puppet::Util::NetworkDevice.current.is_a? Puppet::ResourceApi::Transport::Wrapper
          @my_provider[Puppet::Util::NetworkDevice.current.transport.class]
        else
          @my_provider[Puppet::Util::NetworkDevice.current.class]
        end
      end

      # make the provider available in the instance's namespace
      def my_provider
        self.class.my_provider
      end

      define_singleton_method(:type_definition) do
        @type_definition
      end

      def type_definition
        self.class.type_definition
      end

      if type_definition.feature?('remote_resource')
        apply_to_device
      end

      def initialize(attributes)
        # $stderr.puts "A: #{attributes.inspect}"
        if attributes.is_a? Puppet::Resource
          @title = attributes.title
          @catalog = attributes.catalog
          sensitives = attributes.sensitive_parameters
          attributes = attributes.to_hash
        else
          @ral_find_absent = true
          sensitives = []
        end

        # undo puppet's unwrapping of Sensitive values to provide a uniform experience for providers
        # See https://tickets.puppetlabs.com/browse/PDK-1091 for investigation and background
        sensitives.each do |name|
          if attributes.key?(name) && !attributes[name].is_a?(Puppet::Pops::Types::PSensitiveType::Sensitive)
            attributes[name] = Puppet::Pops::Types::PSensitiveType::Sensitive.new(attributes[name])
          end
        end

        # $stderr.puts "B: #{attributes.inspect}"
        if type_definition.feature?('canonicalize')
          attributes = my_provider.canonicalize(context, [attributes])[0]
        end

        # 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 type_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

      def name
        title
      end

      def self.build_title(type_definition, resource_hash)
        if type_definition.namevars.size > 1
          # use a MonkeyHash to allow searching in Puppet's RAL
          Puppet::ResourceApi::MonkeyHash[type_definition.namevars.map { |attr| [attr, resource_hash[attr]] }]
        else
          resource_hash[type_definition.namevars[0]]
        end
      end

      def rsapi_title
        @rsapi_title ||= self.class.build_title(type_definition, self)
        @rsapi_title
      end

      def rsapi_canonicalized_target_state
        @rsapi_canonicalized_target_state ||= begin
          # skip puppet's injected metaparams
          actual_params = @parameters.select { |k, _v| type_definition.attributes.key? k }
          target_state = Hash[actual_params.map { |k, v| [k, v.rs_value] }]
          target_state = my_provider.canonicalize(context, [target_state]).first if type_definition.feature?('canonicalize')
          target_state
        end
        @rsapi_canonicalized_target_state
      end

      def rsapi_current_state
        refresh_current_state unless @rsapi_current_state
        @rsapi_current_state
      end

      def to_resource
        to_resource_shim(super)
      end

      def to_resource_shim(resource)
        resource_hash = Hash[resource.keys.map { |k| [k, resource[k]] }]
        resource_hash[:title] = resource.title
        ResourceShim.new(resource_hash, type_definition.name, type_definition.namevars, type_definition.attributes, catalog)
      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::ResourceApi::DataTypeHandling.parse_puppet_type(
            :name,
            options[:type],
          )

          # skip read only vars and the namevar
          next if [:read_only, :namevar].include? options[:behaviour]

          # skip properties if the resource is being deleted
          next if definition[:attributes][:ensure] &&
                  value(:ensure) == 'absent' &&
                  options[:behaviour].nil?

          if value(name).nil? && !(type.instance_of? Puppet::Pops::Types::POptionalType)
            @missing_attrs << name
            @missing_params << name if options[:behaviour] == :parameter
          end
        end

        @missing_attrs -= [:ensure]

        raise_missing_params if @missing_params.any?
      end

      # If the custom_insync feature is specified but no insyncable attributes are included
      # in the definition, add the hidden rsapi_custom_insync_trigger property.
      # This property exists *only* to allow a resource without properties to still execute an
      # insync check; there's no point in specifying it in a manifest as it can only have one
      # value; it cannot be specified in a type definition as it should only exist in this case.
      if type_definition.feature?('custom_insync') && type_definition.insyncable_attributes.empty?
        custom_insync_trigger_options = {
          type: 'Enum[do_not_specify_in_manifest]',
          desc: 'A hidden property which enables a type with custom insync to perform an insync check without specifying any insyncable properties',
          default: 'do_not_specify_in_manifest',
        }

        type_definition.create_attribute_in(self, :rsapi_custom_insync_trigger, :newproperty, Puppet::ResourceApi::Property, custom_insync_trigger_options)
      end

      definition[:attributes].each do |name, options|
        # puts "#{name}: #{options.inspect}"

        if options[:behaviour]
          unless [:read_only, :namevar, :parameter, :init_only].include? options[:behaviour]
            raise Puppet::ResourceError, "`#{options[:behaviour]}` is not a valid behaviour value"
          end
        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
        if [:parameter, :namevar].include? options[:behaviour]
          param_or_property = :newparam
          parent = Puppet::ResourceApi::Parameter
        elsif options[:behaviour] == :read_only
          param_or_property = :newparam
          parent = Puppet::ResourceApi::ReadOnlyParameter
        else
          param_or_property = :newproperty
          parent = Puppet::ResourceApi::Property
        end

        type_definition.create_attribute_in(self, name, param_or_property, parent, options)
      end

      def self.instances
        # puts 'instances'
        # force autoloading of the provider
        provider(type_definition.name)

        initial_fetch = if type_definition.feature?('simple_get_filter')
                          my_provider.get(context, [])
                        else
                          my_provider.get(context)
                        end

        initial_fetch.map do |resource_hash|
          type_definition.check_schema(resource_hash)
          # allow a :title from the provider to override the default
          result = if resource_hash.key? :title
                     new(title: resource_hash[:title])
                   else
                     new(title: build_title(type_definition, resource_hash))
                   end
          result.cache_current_state(resource_hash)
          result
        end
      end

      def refresh_current_state
        @rsapi_current_state = if type_definition.feature?('simple_get_filter')
                                 my_provider.get(context, [rsapi_title]).find { |h| namevar_match?(h) }
                               else
                                 my_provider.get(context).find { |h| namevar_match?(h) }
                               end

        if @rsapi_current_state
          type_definition.check_schema(@rsapi_current_state)
          strict_check(@rsapi_current_state)
        else
          @rsapi_current_state = if rsapi_title.is_a? Hash
                                   rsapi_title.dup
                                 else
                                   { title: rsapi_title }
                                 end
          @rsapi_current_state[:ensure] = :absent if type_definition.ensurable?
        end
      end

      # Use this to set the current state from the `instances` method
      def cache_current_state(resource_hash)
        @rsapi_current_state = resource_hash
        strict_check(@rsapi_current_state)
      end

      def retrieve
        Puppet.debug("Current State: #{rsapi_current_state.inspect}")

        result = Puppet::Resource.new(self.class, title, parameters: rsapi_current_state)
        # puppet needs ensure to be a symbol
        result[:ensure] = result[:ensure].to_sym if type_definition.ensurable? && result[:ensure].is_a?(String)

        raise_missing_attrs

        result
      end

      def namevar_match?(item)
        context.type.namevars.all? do |namevar|
          item[namevar] == @parameters[namevar].value if @parameters[namevar].respond_to? :value
        end
      end

      def flush
        raise_missing_attrs

        # puts 'flush'
        target_state = rsapi_canonicalized_target_state

        retrieve unless @rsapi_current_state

        return if @rsapi_current_state == target_state

        Puppet.debug("Target State: #{target_state.inspect}")

        # enforce init_only attributes
        if Puppet.settings[:strict] != :off && @rsapi_current_state && (@rsapi_current_state[:ensure] == 'present' && target_state[:ensure] == 'present')
          target_state.each do |name, value|
            next unless type_definition.attributes[name][:behaviour] == :init_only && value != @rsapi_current_state[name]
            message = "Attempting to change `#{name}` init_only attribute value from `#{@rsapi_current_state[name]}` to `#{value}`"
            case Puppet.settings[:strict]
            when :warning
              Puppet.warning(message)
            when :error
              raise Puppet::ResourceError, message
            end
          end
        end

        if type_definition.feature?('supports_noop')
          my_provider.set(context, { rsapi_title => { is: @rsapi_current_state, should: target_state } }, noop: noop?)
        else
          my_provider.set(context, rsapi_title => { is: @rsapi_current_state, should: target_state }) unless noop?
        end
        if context.failed?
          context.reset_failed
          raise 'Execution encountered an error'
        end

        # remember that we have successfully reached our desired state
        @rsapi_current_state = target_state
      end

      def raise_missing_attrs
        error_msg = "The following mandatory attributes were not provided:\n    *  " + @missing_attrs.join(", \n    *  ")
        raise Puppet::ResourceError, error_msg if @missing_attrs.any? && (value(:ensure) != :absent && !value(:ensure).nil?)
      end

      def raise_missing_params
        error_msg = "The following mandatory parameters were not provided:\n    *  " + @missing_params.join(", \n    *  ")
        raise Puppet::ResourceError, error_msg
      end

      def strict_check(current_state)
        return if Puppet.settings[:strict] == :off

        strict_check_canonicalize(current_state) if type_definition.feature?('canonicalize')
        strict_check_title_parameter(current_state) if type_definition.namevars.size > 1 && !type_definition.title_patterns.empty?

        nil
      end

      def strict_message(message)
        case Puppet.settings[:strict]
        when :warning
          Puppet.warning(message)
        when :error
          raise Puppet::DevError, message
        end
      end

      def strict_check_canonicalize(current_state)
        # if strict checking is on we must notify if the values are changed by canonicalize
        # make a deep copy to perform the operation on and to compare against later
        state_clone = Marshal.load(Marshal.dump(current_state))
        state_clone = my_provider.canonicalize(context, [state_clone]).first

        # compare the clone against the current state to see if changes have been made by canonicalize
        return unless state_clone && (current_state != state_clone)

        #:nocov:
        # codecov fails to register this multiline as covered, even though simplecov does.
        message = <<MESSAGE.strip
#{type_definition.name}[#{@title}]#get has not provided canonicalized values.
Returned values:       #{current_state.inspect}
Canonicalized values:  #{state_clone.inspect}
MESSAGE
        #:nocov:
        strict_message(message)
      end

      def strict_check_title_parameter(current_state)
        unless current_state.key?(:title)
          strict_message("#{type_definition.name}[#{@title}]#get has not provided a title attribute.")
          return
        end

        # Logic borrowed from Puppet::Resource.parse_title
        title_hash = {}
        self.class.title_patterns.each do |regexp, symbols|
          captures = regexp.match(current_state[:title])
          next if captures.nil?
          symbols.zip(captures[1..-1]).each do |symbol_and_lambda, capture|
            # The Resource API does not support passing procs in title_patterns
            # so, unlike Puppet::Resource, we do not need to handle that here.
            symbol = symbol_and_lambda[0]
            title_hash[symbol] = capture
          end
          break
        end

        return if title_hash == rsapi_title

        namevars = type_definition.namevars.reject { |namevar| title_hash[namevar] == rsapi_title[namevar] }

        #:nocov:
        # codecov fails to register this multiline as covered, even though simplecov does.
        message = <<MESSAGE.strip
#{type_definition.name}[#{@title}]#get has provided a title attribute which does not match all namevars.
Namevars which do not match: #{namevars.inspect}
Returned parsed title hash:  #{title_hash.inspect}
Expected hash:               #{rsapi_title.inspect}
MESSAGE
        #:nocov:
        strict_message(message)
      end

      define_singleton_method(:context) do
        @context ||= PuppetContext.new(TypeDefinition.new(definition))
      end

      def context
        self.class.context
      end

      def self.title_patterns
        @title_patterns ||= if type_definition.definition.key? :title_patterns
                              parse_title_patterns(type_definition.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}")
          send(auto, type.downcase.to_sym) do
            resolved = [values].flatten.map do |v|
              match = %r{\A\$(.*)\Z}.match(v) if v.is_a? String
              if match.nil?
                v
              else
                self[match[1].to_sym]
              end
            end
            # Flatten to handle any resolved array properties and filter any nil
            # values resulting from unspecified optional parameters:
            resolved.flatten.reject { |v| v.nil? }
          end
        end
      end
    end
  end
title_patterns() click to toggle source
# File lib/puppet/resource_api.rb, line 428
def self.title_patterns
  @title_patterns ||= if type_definition.definition.key? :title_patterns
                        parse_title_patterns(type_definition.definition[:title_patterns])
                      else
                        [[%r{(.*)}m, [[type_definition.namevars.first]]]]
                      end
end

Public Instance Methods

cache_current_state(resource_hash) click to toggle source

Use this to set the current state from the ‘instances` method

# File lib/puppet/resource_api.rb, line 273
def cache_current_state(resource_hash)
  @rsapi_current_state = resource_hash
  strict_check(@rsapi_current_state)
end
context() click to toggle source
# File lib/puppet/resource_api.rb, line 424
def context
  self.class.context
end
flush() click to toggle source
# File lib/puppet/resource_api.rb, line 296
def flush
  raise_missing_attrs

  # puts 'flush'
  target_state = rsapi_canonicalized_target_state

  retrieve unless @rsapi_current_state

  return if @rsapi_current_state == target_state

  Puppet.debug("Target State: #{target_state.inspect}")

  # enforce init_only attributes
  if Puppet.settings[:strict] != :off && @rsapi_current_state && (@rsapi_current_state[:ensure] == 'present' && target_state[:ensure] == 'present')
    target_state.each do |name, value|
      next unless type_definition.attributes[name][:behaviour] == :init_only && value != @rsapi_current_state[name]
      message = "Attempting to change `#{name}` init_only attribute value from `#{@rsapi_current_state[name]}` to `#{value}`"
      case Puppet.settings[:strict]
      when :warning
        Puppet.warning(message)
      when :error
        raise Puppet::ResourceError, message
      end
    end
  end

  if type_definition.feature?('supports_noop')
    my_provider.set(context, { rsapi_title => { is: @rsapi_current_state, should: target_state } }, noop: noop?)
  else
    my_provider.set(context, rsapi_title => { is: @rsapi_current_state, should: target_state }) unless noop?
  end
  if context.failed?
    context.reset_failed
    raise 'Execution encountered an error'
  end

  # remember that we have successfully reached our desired state
  @rsapi_current_state = target_state
end
my_provider() click to toggle source

make the provider available in the instance’s namespace

# File lib/puppet/resource_api.rb, line 56
def my_provider
  self.class.my_provider
end
name() click to toggle source
# File lib/puppet/resource_api.rb, line 110
def name
  title
end
namevar_match?(item) click to toggle source
# File lib/puppet/resource_api.rb, line 290
def namevar_match?(item)
  context.type.namevars.all? do |namevar|
    item[namevar] == @parameters[namevar].value if @parameters[namevar].respond_to? :value
  end
end
raise_missing_attrs() click to toggle source
# File lib/puppet/resource_api.rb, line 336
def raise_missing_attrs
  error_msg = "The following mandatory attributes were not provided:\n    *  " + @missing_attrs.join(", \n    *  ")
  raise Puppet::ResourceError, error_msg if @missing_attrs.any? && (value(:ensure) != :absent && !value(:ensure).nil?)
end
raise_missing_params() click to toggle source
# File lib/puppet/resource_api.rb, line 341
def raise_missing_params
  error_msg = "The following mandatory parameters were not provided:\n    *  " + @missing_params.join(", \n    *  ")
  raise Puppet::ResourceError, error_msg
end
refresh_current_state() click to toggle source
# File lib/puppet/resource_api.rb, line 252
def refresh_current_state
  @rsapi_current_state = if type_definition.feature?('simple_get_filter')
                           my_provider.get(context, [rsapi_title]).find { |h| namevar_match?(h) }
                         else
                           my_provider.get(context).find { |h| namevar_match?(h) }
                         end

  if @rsapi_current_state
    type_definition.check_schema(@rsapi_current_state)
    strict_check(@rsapi_current_state)
  else
    @rsapi_current_state = if rsapi_title.is_a? Hash
                             rsapi_title.dup
                           else
                             { title: rsapi_title }
                           end
    @rsapi_current_state[:ensure] = :absent if type_definition.ensurable?
  end
end
retrieve() click to toggle source
# File lib/puppet/resource_api.rb, line 278
def retrieve
  Puppet.debug("Current State: #{rsapi_current_state.inspect}")

  result = Puppet::Resource.new(self.class, title, parameters: rsapi_current_state)
  # puppet needs ensure to be a symbol
  result[:ensure] = result[:ensure].to_sym if type_definition.ensurable? && result[:ensure].is_a?(String)

  raise_missing_attrs

  result
end
rsapi_canonicalized_target_state() click to toggle source
# File lib/puppet/resource_api.rb, line 128
def rsapi_canonicalized_target_state
  @rsapi_canonicalized_target_state ||= begin
    # skip puppet's injected metaparams
    actual_params = @parameters.select { |k, _v| type_definition.attributes.key? k }
    target_state = Hash[actual_params.map { |k, v| [k, v.rs_value] }]
    target_state = my_provider.canonicalize(context, [target_state]).first if type_definition.feature?('canonicalize')
    target_state
  end
  @rsapi_canonicalized_target_state
end
rsapi_current_state() click to toggle source
# File lib/puppet/resource_api.rb, line 139
def rsapi_current_state
  refresh_current_state unless @rsapi_current_state
  @rsapi_current_state
end
rsapi_title() click to toggle source
# File lib/puppet/resource_api.rb, line 123
def rsapi_title
  @rsapi_title ||= self.class.build_title(type_definition, self)
  @rsapi_title
end
strict_check(current_state) click to toggle source
# File lib/puppet/resource_api.rb, line 346
def strict_check(current_state)
  return if Puppet.settings[:strict] == :off

  strict_check_canonicalize(current_state) if type_definition.feature?('canonicalize')
  strict_check_title_parameter(current_state) if type_definition.namevars.size > 1 && !type_definition.title_patterns.empty?

  nil
end
strict_check_canonicalize(current_state) click to toggle source
# File lib/puppet/resource_api.rb, line 364
      def strict_check_canonicalize(current_state)
        # if strict checking is on we must notify if the values are changed by canonicalize
        # make a deep copy to perform the operation on and to compare against later
        state_clone = Marshal.load(Marshal.dump(current_state))
        state_clone = my_provider.canonicalize(context, [state_clone]).first

        # compare the clone against the current state to see if changes have been made by canonicalize
        return unless state_clone && (current_state != state_clone)

        #:nocov:
        # codecov fails to register this multiline as covered, even though simplecov does.
        message = <<MESSAGE.strip
#{type_definition.name}[#{@title}]#get has not provided canonicalized values.
Returned values:       #{current_state.inspect}
Canonicalized values:  #{state_clone.inspect}
MESSAGE
        #:nocov:
        strict_message(message)
      end
strict_check_title_parameter(current_state) click to toggle source
# File lib/puppet/resource_api.rb, line 384
      def strict_check_title_parameter(current_state)
        unless current_state.key?(:title)
          strict_message("#{type_definition.name}[#{@title}]#get has not provided a title attribute.")
          return
        end

        # Logic borrowed from Puppet::Resource.parse_title
        title_hash = {}
        self.class.title_patterns.each do |regexp, symbols|
          captures = regexp.match(current_state[:title])
          next if captures.nil?
          symbols.zip(captures[1..-1]).each do |symbol_and_lambda, capture|
            # The Resource API does not support passing procs in title_patterns
            # so, unlike Puppet::Resource, we do not need to handle that here.
            symbol = symbol_and_lambda[0]
            title_hash[symbol] = capture
          end
          break
        end

        return if title_hash == rsapi_title

        namevars = type_definition.namevars.reject { |namevar| title_hash[namevar] == rsapi_title[namevar] }

        #:nocov:
        # codecov fails to register this multiline as covered, even though simplecov does.
        message = <<MESSAGE.strip
#{type_definition.name}[#{@title}]#get has provided a title attribute which does not match all namevars.
Namevars which do not match: #{namevars.inspect}
Returned parsed title hash:  #{title_hash.inspect}
Expected hash:               #{rsapi_title.inspect}
MESSAGE
        #:nocov:
        strict_message(message)
      end
strict_message(message) click to toggle source
# File lib/puppet/resource_api.rb, line 355
def strict_message(message)
  case Puppet.settings[:strict]
  when :warning
    Puppet.warning(message)
  when :error
    raise Puppet::DevError, message
  end
end
to_resource() click to toggle source
Calls superclass method
# File lib/puppet/resource_api.rb, line 144
def to_resource
  to_resource_shim(super)
end
to_resource_shim(resource) click to toggle source
# File lib/puppet/resource_api.rb, line 148
def to_resource_shim(resource)
  resource_hash = Hash[resource.keys.map { |k| [k, resource[k]] }]
  resource_hash[:title] = resource.title
  ResourceShim.new(resource_hash, type_definition.name, type_definition.namevars, type_definition.attributes, catalog)
end
type_definition() click to toggle source
# File lib/puppet/resource_api.rb, line 64
def type_definition
  self.class.type_definition
end

Private Instance Methods

load_default_provider(class_name, type_name_sym) click to toggle source
# File lib/puppet/resource_api.rb, line 506
def load_default_provider(class_name, type_name_sym)
  # loads the "puppet/provider/#{type_name}/#{type_name}" file through puppet
  Puppet::Type.type(type_name_sym).provider(type_name_sym)
  Puppet::Provider.const_get(class_name, false).const_get(class_name, false)
end
load_device_provider(class_name, type_name_sym, device_class_name, device_name_sym) click to toggle source
# File lib/puppet/resource_api.rb, line 513
def load_device_provider(class_name, type_name_sym, device_class_name, device_name_sym)
  # loads the "puppet/provider/#{type_name}/#{device_name}" file through puppet
  Puppet::Type.type(type_name_sym).provider(device_name_sym)
  provider_module = Puppet::Provider.const_get(class_name, false)
  if provider_module.const_defined?(device_class_name, false)
    provider_module.const_get(device_class_name, false)
  else
    load_default_provider(class_name, type_name_sym)
  end
end
load_provider(type_name) click to toggle source
# File lib/puppet/resource_api.rb, line 477
def load_provider(type_name)
  class_name = class_name_from_type_name(type_name)
  type_name_sym = type_name.to_sym
  device_name = if Puppet::Util::NetworkDevice.current.nil?
                  nil
                elsif Puppet::Util::NetworkDevice.current.is_a? Puppet::ResourceApi::Transport::Wrapper
                  # extract the device type from the currently loaded device's class
                  Puppet::Util::NetworkDevice.current.schema.name
                else
                  Puppet::Util::NetworkDevice.current.class.name.split('::')[-2].downcase
                end
  device_class_name = class_name_from_type_name(device_name)

  if device_name
    device_name_sym = device_name.to_sym if device_name
    load_device_provider(class_name, type_name_sym, device_class_name, device_name_sym)
  else
    load_default_provider(class_name, type_name_sym)
  end
rescue NameError
  if device_name # line too long # rubocop:disable Style/GuardClause
    raise Puppet::DevError, "Found neither the device-specific provider class Puppet::Provider::#{class_name}::#{device_class_name} in puppet/provider/#{type_name}/#{device_name}"\
    " nor the generic provider class Puppet::Provider::#{class_name}::#{class_name} in puppet/provider/#{type_name}/#{type_name}"
  else
    raise Puppet::DevError, "provider class Puppet::Provider::#{class_name}::#{class_name} not found in puppet/provider/#{type_name}/#{type_name}"
  end
end
register_transport(schema) click to toggle source

keeps the existing register API format. e.g. Puppet::ResourceApi.register_type

# File lib/puppet/resource_api.rb, line 526
def register_transport(schema)
  Puppet::ResourceApi::Transport.register(schema)
end
register_type(definition) click to toggle source
# File lib/puppet/resource_api.rb, line 26
  def register_type(definition)
    # Attempt to create a TypeDefinition from the input hash
    # This will validate and throw if its not right
    type_def = TypeDefinition.new(definition)

    # prepare the ruby module for the provider
    # this has to happen before Puppet::Type.newtype starts autoloading providers
    # it also needs to be guarded against the namespace already being defined by something
    # else to avoid ruby warnings
    unless Puppet::Provider.const_defined?(class_name_from_type_name(definition[:name]), false)
      Puppet::Provider.const_set(class_name_from_type_name(definition[:name]), Module.new)
    end

    Puppet::Type.newtype(definition[:name].to_sym) do
      # The :desc value is already cleaned up by the TypeDefinition validation
      @doc = definition[:desc]
      @type_definition = type_def

      # 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 ||= Hash.new { |hash, key| hash[key] = Puppet::ResourceApi.load_provider(definition[:name]).new }

        if Puppet::Util::NetworkDevice.current.is_a? Puppet::ResourceApi::Transport::Wrapper
          @my_provider[Puppet::Util::NetworkDevice.current.transport.class]
        else
          @my_provider[Puppet::Util::NetworkDevice.current.class]
        end
      end

      # make the provider available in the instance's namespace
      def my_provider
        self.class.my_provider
      end

      define_singleton_method(:type_definition) do
        @type_definition
      end

      def type_definition
        self.class.type_definition
      end

      if type_definition.feature?('remote_resource')
        apply_to_device
      end

      def initialize(attributes)
        # $stderr.puts "A: #{attributes.inspect}"
        if attributes.is_a? Puppet::Resource
          @title = attributes.title
          @catalog = attributes.catalog
          sensitives = attributes.sensitive_parameters
          attributes = attributes.to_hash
        else
          @ral_find_absent = true
          sensitives = []
        end

        # undo puppet's unwrapping of Sensitive values to provide a uniform experience for providers
        # See https://tickets.puppetlabs.com/browse/PDK-1091 for investigation and background
        sensitives.each do |name|
          if attributes.key?(name) && !attributes[name].is_a?(Puppet::Pops::Types::PSensitiveType::Sensitive)
            attributes[name] = Puppet::Pops::Types::PSensitiveType::Sensitive.new(attributes[name])
          end
        end

        # $stderr.puts "B: #{attributes.inspect}"
        if type_definition.feature?('canonicalize')
          attributes = my_provider.canonicalize(context, [attributes])[0]
        end

        # 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 type_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

      def name
        title
      end

      def self.build_title(type_definition, resource_hash)
        if type_definition.namevars.size > 1
          # use a MonkeyHash to allow searching in Puppet's RAL
          Puppet::ResourceApi::MonkeyHash[type_definition.namevars.map { |attr| [attr, resource_hash[attr]] }]
        else
          resource_hash[type_definition.namevars[0]]
        end
      end

      def rsapi_title
        @rsapi_title ||= self.class.build_title(type_definition, self)
        @rsapi_title
      end

      def rsapi_canonicalized_target_state
        @rsapi_canonicalized_target_state ||= begin
          # skip puppet's injected metaparams
          actual_params = @parameters.select { |k, _v| type_definition.attributes.key? k }
          target_state = Hash[actual_params.map { |k, v| [k, v.rs_value] }]
          target_state = my_provider.canonicalize(context, [target_state]).first if type_definition.feature?('canonicalize')
          target_state
        end
        @rsapi_canonicalized_target_state
      end

      def rsapi_current_state
        refresh_current_state unless @rsapi_current_state
        @rsapi_current_state
      end

      def to_resource
        to_resource_shim(super)
      end

      def to_resource_shim(resource)
        resource_hash = Hash[resource.keys.map { |k| [k, resource[k]] }]
        resource_hash[:title] = resource.title
        ResourceShim.new(resource_hash, type_definition.name, type_definition.namevars, type_definition.attributes, catalog)
      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::ResourceApi::DataTypeHandling.parse_puppet_type(
            :name,
            options[:type],
          )

          # skip read only vars and the namevar
          next if [:read_only, :namevar].include? options[:behaviour]

          # skip properties if the resource is being deleted
          next if definition[:attributes][:ensure] &&
                  value(:ensure) == 'absent' &&
                  options[:behaviour].nil?

          if value(name).nil? && !(type.instance_of? Puppet::Pops::Types::POptionalType)
            @missing_attrs << name
            @missing_params << name if options[:behaviour] == :parameter
          end
        end

        @missing_attrs -= [:ensure]

        raise_missing_params if @missing_params.any?
      end

      # If the custom_insync feature is specified but no insyncable attributes are included
      # in the definition, add the hidden rsapi_custom_insync_trigger property.
      # This property exists *only* to allow a resource without properties to still execute an
      # insync check; there's no point in specifying it in a manifest as it can only have one
      # value; it cannot be specified in a type definition as it should only exist in this case.
      if type_definition.feature?('custom_insync') && type_definition.insyncable_attributes.empty?
        custom_insync_trigger_options = {
          type: 'Enum[do_not_specify_in_manifest]',
          desc: 'A hidden property which enables a type with custom insync to perform an insync check without specifying any insyncable properties',
          default: 'do_not_specify_in_manifest',
        }

        type_definition.create_attribute_in(self, :rsapi_custom_insync_trigger, :newproperty, Puppet::ResourceApi::Property, custom_insync_trigger_options)
      end

      definition[:attributes].each do |name, options|
        # puts "#{name}: #{options.inspect}"

        if options[:behaviour]
          unless [:read_only, :namevar, :parameter, :init_only].include? options[:behaviour]
            raise Puppet::ResourceError, "`#{options[:behaviour]}` is not a valid behaviour value"
          end
        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
        if [:parameter, :namevar].include? options[:behaviour]
          param_or_property = :newparam
          parent = Puppet::ResourceApi::Parameter
        elsif options[:behaviour] == :read_only
          param_or_property = :newparam
          parent = Puppet::ResourceApi::ReadOnlyParameter
        else
          param_or_property = :newproperty
          parent = Puppet::ResourceApi::Property
        end

        type_definition.create_attribute_in(self, name, param_or_property, parent, options)
      end

      def self.instances
        # puts 'instances'
        # force autoloading of the provider
        provider(type_definition.name)

        initial_fetch = if type_definition.feature?('simple_get_filter')
                          my_provider.get(context, [])
                        else
                          my_provider.get(context)
                        end

        initial_fetch.map do |resource_hash|
          type_definition.check_schema(resource_hash)
          # allow a :title from the provider to override the default
          result = if resource_hash.key? :title
                     new(title: resource_hash[:title])
                   else
                     new(title: build_title(type_definition, resource_hash))
                   end
          result.cache_current_state(resource_hash)
          result
        end
      end

      def refresh_current_state
        @rsapi_current_state = if type_definition.feature?('simple_get_filter')
                                 my_provider.get(context, [rsapi_title]).find { |h| namevar_match?(h) }
                               else
                                 my_provider.get(context).find { |h| namevar_match?(h) }
                               end

        if @rsapi_current_state
          type_definition.check_schema(@rsapi_current_state)
          strict_check(@rsapi_current_state)
        else
          @rsapi_current_state = if rsapi_title.is_a? Hash
                                   rsapi_title.dup
                                 else
                                   { title: rsapi_title }
                                 end
          @rsapi_current_state[:ensure] = :absent if type_definition.ensurable?
        end
      end

      # Use this to set the current state from the `instances` method
      def cache_current_state(resource_hash)
        @rsapi_current_state = resource_hash
        strict_check(@rsapi_current_state)
      end

      def retrieve
        Puppet.debug("Current State: #{rsapi_current_state.inspect}")

        result = Puppet::Resource.new(self.class, title, parameters: rsapi_current_state)
        # puppet needs ensure to be a symbol
        result[:ensure] = result[:ensure].to_sym if type_definition.ensurable? && result[:ensure].is_a?(String)

        raise_missing_attrs

        result
      end

      def namevar_match?(item)
        context.type.namevars.all? do |namevar|
          item[namevar] == @parameters[namevar].value if @parameters[namevar].respond_to? :value
        end
      end

      def flush
        raise_missing_attrs

        # puts 'flush'
        target_state = rsapi_canonicalized_target_state

        retrieve unless @rsapi_current_state

        return if @rsapi_current_state == target_state

        Puppet.debug("Target State: #{target_state.inspect}")

        # enforce init_only attributes
        if Puppet.settings[:strict] != :off && @rsapi_current_state && (@rsapi_current_state[:ensure] == 'present' && target_state[:ensure] == 'present')
          target_state.each do |name, value|
            next unless type_definition.attributes[name][:behaviour] == :init_only && value != @rsapi_current_state[name]
            message = "Attempting to change `#{name}` init_only attribute value from `#{@rsapi_current_state[name]}` to `#{value}`"
            case Puppet.settings[:strict]
            when :warning
              Puppet.warning(message)
            when :error
              raise Puppet::ResourceError, message
            end
          end
        end

        if type_definition.feature?('supports_noop')
          my_provider.set(context, { rsapi_title => { is: @rsapi_current_state, should: target_state } }, noop: noop?)
        else
          my_provider.set(context, rsapi_title => { is: @rsapi_current_state, should: target_state }) unless noop?
        end
        if context.failed?
          context.reset_failed
          raise 'Execution encountered an error'
        end

        # remember that we have successfully reached our desired state
        @rsapi_current_state = target_state
      end

      def raise_missing_attrs
        error_msg = "The following mandatory attributes were not provided:\n    *  " + @missing_attrs.join(", \n    *  ")
        raise Puppet::ResourceError, error_msg if @missing_attrs.any? && (value(:ensure) != :absent && !value(:ensure).nil?)
      end

      def raise_missing_params
        error_msg = "The following mandatory parameters were not provided:\n    *  " + @missing_params.join(", \n    *  ")
        raise Puppet::ResourceError, error_msg
      end

      def strict_check(current_state)
        return if Puppet.settings[:strict] == :off

        strict_check_canonicalize(current_state) if type_definition.feature?('canonicalize')
        strict_check_title_parameter(current_state) if type_definition.namevars.size > 1 && !type_definition.title_patterns.empty?

        nil
      end

      def strict_message(message)
        case Puppet.settings[:strict]
        when :warning
          Puppet.warning(message)
        when :error
          raise Puppet::DevError, message
        end
      end

      def strict_check_canonicalize(current_state)
        # if strict checking is on we must notify if the values are changed by canonicalize
        # make a deep copy to perform the operation on and to compare against later
        state_clone = Marshal.load(Marshal.dump(current_state))
        state_clone = my_provider.canonicalize(context, [state_clone]).first

        # compare the clone against the current state to see if changes have been made by canonicalize
        return unless state_clone && (current_state != state_clone)

        #:nocov:
        # codecov fails to register this multiline as covered, even though simplecov does.
        message = <<MESSAGE.strip
#{type_definition.name}[#{@title}]#get has not provided canonicalized values.
Returned values:       #{current_state.inspect}
Canonicalized values:  #{state_clone.inspect}
MESSAGE
        #:nocov:
        strict_message(message)
      end

      def strict_check_title_parameter(current_state)
        unless current_state.key?(:title)
          strict_message("#{type_definition.name}[#{@title}]#get has not provided a title attribute.")
          return
        end

        # Logic borrowed from Puppet::Resource.parse_title
        title_hash = {}
        self.class.title_patterns.each do |regexp, symbols|
          captures = regexp.match(current_state[:title])
          next if captures.nil?
          symbols.zip(captures[1..-1]).each do |symbol_and_lambda, capture|
            # The Resource API does not support passing procs in title_patterns
            # so, unlike Puppet::Resource, we do not need to handle that here.
            symbol = symbol_and_lambda[0]
            title_hash[symbol] = capture
          end
          break
        end

        return if title_hash == rsapi_title

        namevars = type_definition.namevars.reject { |namevar| title_hash[namevar] == rsapi_title[namevar] }

        #:nocov:
        # codecov fails to register this multiline as covered, even though simplecov does.
        message = <<MESSAGE.strip
#{type_definition.name}[#{@title}]#get has provided a title attribute which does not match all namevars.
Namevars which do not match: #{namevars.inspect}
Returned parsed title hash:  #{title_hash.inspect}
Expected hash:               #{rsapi_title.inspect}
MESSAGE
        #:nocov:
        strict_message(message)
      end

      define_singleton_method(:context) do
        @context ||= PuppetContext.new(TypeDefinition.new(definition))
      end

      def context
        self.class.context
      end

      def self.title_patterns
        @title_patterns ||= if type_definition.definition.key? :title_patterns
                              parse_title_patterns(type_definition.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}")
          send(auto, type.downcase.to_sym) do
            resolved = [values].flatten.map do |v|
              match = %r{\A\$(.*)\Z}.match(v) if v.is_a? String
              if match.nil?
                v
              else
                self[match[1].to_sym]
              end
            end
            # Flatten to handle any resolved array properties and filter any nil
            # values resulting from unspecified optional parameters:
            resolved.flatten.reject { |v| v.nil? }
          end
        end
      end
    end
  end