Module: Puppet::ResourceApi

Defined in:
lib/puppet/resource_api/version.rb,
lib/puppet/resource_api/simple_provider.rb,
lib/puppet/resource_api/command.rb,
lib/puppet/resource_api/errors.rb,
lib/puppet/resource_api/glue.rb,
lib/puppet/resource_api.rb

Defined Under Namespace

Classes: BaseContext, Command, CommandExecutionError, CommandNotFoundError, IOContext, PuppetContext, ResourceShim, SimpleProvider, TypeShim

Constant Summary collapse

VERSION =
'0.2.1'.freeze

Class Method Summary collapse

Class Method Details

.class_name_from_type_name(type_name) ⇒ Object



227
228
229
# File 'lib/puppet/resource_api.rb', line 227

def self.class_name_from_type_name(type_name)
  type_name.to_s.split('_').map(&:capitalize).join
end

.def_newvalues(this, param_or_property, *values) ⇒ Object

Add the value to ‘this` property or param, depending on whether param_or_property is `:newparam`, or `:newproperty`



232
233
234
235
236
237
238
239
240
# File 'lib/puppet/resource_api.rb', line 232

def self.def_newvalues(this, param_or_property, *values)
  if param_or_property == :newparam
    this.newvalues(*values)
  else
    values.each do |v|
      this.newvalue(v) {}
    end
  end
end

.load_provider(type_name) ⇒ Object



215
216
217
218
219
220
221
222
223
224
# File 'lib/puppet/resource_api.rb', line 215

def load_provider(type_name)
  class_name = class_name_from_type_name(type_name)
  type_name_sym = type_name.to_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).const_get(class_name)
rescue NameError
  raise Puppet::DevError, "class #{class_name} not found in puppet/provider/#{type_name}/#{type_name}"
end

.register_type(definition) ⇒ Object

Raises:

  • (Puppet::DevError)


10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/puppet/resource_api.rb', line 10

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

  # 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]))
    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

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

    if definition[:features] && definition[:features].include?('remote_resource')
      apply_to_device
    end

    define_method(:initialize) do |attributes|
      # $stderr.puts "A: #{attributes.inspect}"
      attributes = attributes.to_hash if attributes.is_a? Puppet::Resource
      # $stderr.puts "B: #{attributes.inspect}"
      if definition.key?(:features) && definition[:features].include?('canonicalize')
        attributes = my_provider.canonicalize(context, [attributes])[0]
      end
      # $stderr.puts "C: #{attributes.inspect}"
      super(attributes)
    end

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

      # 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]
                            :newparam
                          else
                            :newproperty
                          end
      send(param_or_property, name.to_sym) do
        unless options[:type]
          raise Puppet::DevError, "#{definition[:name]}.#{name} has no type"
        end

        if options[:desc]
          desc "#{options[:desc]} (a #{options[:type]})"
        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
          # TODO: this should use Pops infrastructure to avoid hardcoding stuff, and enhance type fidelity
          # validate do |v|
          #   type = Puppet::Pops::Types::TypeParser.singleton.parse(options[:type]).normalize
          #   if type.instance?(v)
          #     return true
          #   else
          #     inferred_type = Puppet::Pops::Types::TypeCalculator.infer_set(value)
          #     error_msg = Puppet::Pops::Types::TypeMismatchDescriber.new.describe_mismatch("#{DEFINITION[:name]}.#{name}", type, inferred_type)
          #     raise Puppet::ResourceError, error_msg
          #   end
          # end

          if options.key? :default
            defaultto options[:default]
          end

          case options[:type]
          when 'String'
            # require any string value
            Puppet::ResourceApi.def_newvalues(self, param_or_property, %r{})
          # rubocop:disable Lint/BooleanSymbol
          when 'Boolean'
            Puppet::ResourceApi.def_newvalues(self, param_or_property, 'true', 'false')
            aliasvalue true, 'true'
            aliasvalue false, 'false'
            aliasvalue :true, 'true'
            aliasvalue :false, 'false'

            munge do |v|
              case v
              when 'true', :true
                true
              when 'false', :false
                false
              else
                v
              end
            end
          # rubocop:enable Lint/BooleanSymbol
          when 'Integer'
            Puppet::ResourceApi.def_newvalues(self, param_or_property, %r{^-?\d+$})
            munge do |v|
              Puppet::Pops::Utils.to_n(v)
            end
          when 'Float', 'Numeric'
            Puppet::ResourceApi.def_newvalues(self, param_or_property, Puppet::Pops::Patterns::NUMERIC)
            munge do |v|
              Puppet::Pops::Utils.to_n(v)
            end
          when 'Enum[present, absent]'
            Puppet::ResourceApi.def_newvalues(self, param_or_property, :absent, :present)
          when 'Variant[Pattern[/\A(0x)?[0-9a-fA-F]{8}\Z/], Pattern[/\A(0x)?[0-9a-fA-F]{16}\Z/], Pattern[/\A(0x)?[0-9a-fA-F]{40}\Z/]]'
            # the namevar needs to be a Parameter, which only has newvalue*s*
            Puppet::ResourceApi.def_newvalues(self, param_or_property, %r{\A(0x)?[0-9a-fA-F]{8}\Z}, %r{\A(0x)?[0-9a-fA-F]{16}\Z}, %r{\A(0x)?[0-9a-fA-F]{40}\Z})
          when 'Optional[String]'
            Puppet::ResourceApi.def_newvalues(self, param_or_property, %r{}, :undef)
          when 'Variant[Stdlib::Absolutepath, Pattern[/\A(https?|ftp):\/\//]]'
            # TODO: this is wrong, but matches original implementation
            Puppet::ResourceApi.def_newvalues(self, param_or_property, /^\//, /\A(https?|ftp):\/\//) # rubocop:disable Style/RegexpLiteral
          when 'Pattern[/\A((hkp|http|https):\/\/)?([a-z\d])([a-z\d-]{0,61}\.)+[a-z\d]+(:\d{2,5})?$/]'
            Puppet::ResourceApi.def_newvalues(self, param_or_property, /\A((hkp|http|https):\/\/)?([a-z\d])([a-z\d-]{0,61}\.)+[a-z\d]+(:\d{2,5})?$/) # rubocop:disable Style/RegexpLiteral
          else
            raise Puppet::DevError, "Datatype #{options[:type]} is not yet supported in this prototype"
          end
        end
      end
    end

    define_singleton_method(:instances) do
      # puts 'instances'
      # force autoloading of the provider
      provider(name)
      my_provider.get(context).map do |resource_hash|
        Puppet::ResourceApi::TypeShim.new(resource_hash[namevar_name], resource_hash)
      end
    end

    define_method(:retrieve) do
      # puts "retrieve(#{title.inspect})"
      result        = Puppet::Resource.new(self.class, title)
      current_state = my_provider.get(context).find { |h| h[namevar_name] == title }

      # require 'pry'; binding.pry

      if current_state
        current_state.each do |k, v|
          result[k] = v
        end
      else
        result[:name] = title
        result[:ensure] = :absent
      end

      # puts "retrieved #{current_state.inspect}"

      @rapi_current_state = current_state
      result
    end

    define_method(:flush) do
      # puts 'flush'
      # require'pry';binding.pry
      target_state = Hash[@parameters.map { |k, v| [k, v.value] }]
      # remove puppet's injected metaparams
      target_state.delete(:loglevel)
      target_state = my_provider.canonicalize(context, [target_state]).first if definition.key?(:features) && definition[:features].include?('canonicalize')

      retrieve unless @rapi_current_state

      # require 'pry'; binding.pry
      return if @rapi_current_state == target_state

      # puts "@rapi_current_state: #{@rapi_current_state.inspect}"
      # puts "target_state: #{target_state.inspect}"

      my_provider.set(context, title => { is: @rapi_current_state, should: target_state })
      raise 'Execution encountered an error' if context.failed?
    end

    define_singleton_method(:context) do
      @context ||= PuppetContext.new(definition[:name])
    end

    def context
      self.class.context
    end
  end
end