1 2 3 4 5 6 7 8 9 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 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
|
# frozen_string_literal: true
require_relative '../../puppet/util/autoload'
require_relative '../../puppet/parser/scope'
require_relative '../../puppet/pops/adaptable'
require_relative '../../puppet/concurrent/lock'
# A module for managing parser functions. Each specified function
# is added to a central module that then gets included into the Scope
# class.
#
# @api public
module Puppet::Parser::Functions
Environment = Puppet::Node::Environment
class << self
include Puppet::Util
end
# Reset the list of loaded functions.
#
# @api private
def self.reset
# Runs a newfunction to create a function for each of the log levels
root_env = Puppet.lookup(:root_environment)
AnonymousModuleAdapter.clear(root_env)
Puppet::Util::Log.levels.each do |level|
newfunction(level,
:environment => root_env,
:doc => "Log a message on the server at level #{level}.") do |vals|
send(level, vals.join(" "))
end
end
end
class AutoloaderDelegate
attr_reader :delegatee
def initialize
@delegatee = Puppet::Util::Autoload.new(self, "puppet/parser/functions")
end
def loadall(env = Puppet.lookup(:current_environment))
if Puppet[:strict] != :off
Puppet.warn_once('deprecations', 'Puppet::Parser::Functions#loadall', _("The method 'Puppet::Parser::Functions.autoloader#loadall' is deprecated in favor of using 'Scope#call_function'."))
end
@delegatee.loadall(env)
end
def load(name, env = Puppet.lookup(:current_environment))
if Puppet[:strict] != :off
Puppet.warn_once('deprecations', "Puppet::Parser::Functions#load('#{name}')", _("The method 'Puppet::Parser::Functions.autoloader#load(\"%{name}\")' is deprecated in favor of using 'Scope#call_function'.") % { name: name })
end
@delegatee.load(name, env)
end
def loaded?(name)
if Puppet[:strict] != :off
Puppet.warn_once('deprecations', "Puppet::Parser::Functions.loaded?('#{name}')", _("The method 'Puppet::Parser::Functions.autoloader#loaded?(\"%{name}\")' is deprecated in favor of using 'Scope#call_function'.") % { name: name })
end
@delegatee.loaded?(name)
end
end
# Accessor for singleton autoloader
#
# @api private
def self.autoloader
@autoloader ||= AutoloaderDelegate.new
end
# An adapter that ties the anonymous module that acts as the container for all 3x functions to the environment
# where the functions are created. This adapter ensures that the life-cycle of those functions doesn't exceed
# the life-cycle of the environment.
#
# @api private
class AnonymousModuleAdapter < Puppet::Pops::Adaptable::Adapter
attr_accessor :module
def self.create_adapter(env)
adapter = super(env)
adapter.module = Module.new do
@metadata = {}
def self.all_function_info
@metadata
end
def self.get_function_info(name)
@metadata[name]
end
def self.add_function_info(name, info)
@metadata[name] = info
end
end
adapter
end
end
@environment_module_lock = Puppet::Concurrent::Lock.new
# Get the module that functions are mixed into corresponding to an
# environment
#
# @api private
def self.environment_module(env)
@environment_module_lock.synchronize do
AnonymousModuleAdapter.adapt(env).module
end
end
# Create a new Puppet DSL function.
#
# **The {newfunction} method provides a public API.**
#
# This method is used both internally inside of Puppet to define parser
# functions. For example, template() is defined in
# {file:lib/puppet/parser/functions/template.rb template.rb} using the
# {newfunction} method. Third party Puppet modules such as
# [stdlib](https://forge.puppetlabs.com/puppetlabs/stdlib) use this method to
# extend the behavior and functionality of Puppet.
#
# See also [Docs: Custom
# Functions](https://puppet.com/docs/puppet/5.5/lang_write_functions_in_puppet.html)
#
# @example Define a new Puppet DSL Function
# >> Puppet::Parser::Functions.newfunction(:double, :arity => 1,
# :doc => "Doubles an object, typically a number or string.",
# :type => :rvalue) {|i| i[0]*2 }
# => {:arity=>1, :type=>:rvalue,
# :name=>"function_double",
# :doc=>"Doubles an object, typically a number or string."}
#
# @example Invoke the double function from irb as is done in RSpec examples:
# >> require 'puppet_spec/scope'
# >> scope = PuppetSpec::Scope.create_test_scope_for_node('example')
# => Scope()
# >> scope.function_double([2])
# => 4
# >> scope.function_double([4])
# => 8
# >> scope.function_double([])
# ArgumentError: double(): Wrong number of arguments given (0 for 1)
# >> scope.function_double([4,8])
# ArgumentError: double(): Wrong number of arguments given (2 for 1)
# >> scope.function_double(["hello"])
# => "hellohello"
#
# @param [Symbol] name the name of the function represented as a ruby Symbol.
# The {newfunction} method will define a Ruby method based on this name on
# the parser scope instance.
#
# @param [Proc] block the block provided to the {newfunction} method will be
# executed when the Puppet DSL function is evaluated during catalog
# compilation. The arguments to the function will be passed as an array to
# the first argument of the block. The return value of the block will be
# the return value of the Puppet DSL function for `:rvalue` functions.
#
# @option options [:rvalue, :statement] :type (:statement) the type of function.
# Either `:rvalue` for functions that return a value, or `:statement` for
# functions that do not return a value.
#
# @option options [String] :doc ('') the documentation for the function.
# This string will be extracted by documentation generation tools.
#
# @option options [Integer] :arity (-1) the
# [arity](https://en.wikipedia.org/wiki/Arity) of the function. When
# specified as a positive integer the function is expected to receive
# _exactly_ the specified number of arguments. When specified as a
# negative number, the function is expected to receive _at least_ the
# absolute value of the specified number of arguments incremented by one.
# For example, a function with an arity of `-4` is expected to receive at
# minimum 3 arguments. A function with the default arity of `-1` accepts
# zero or more arguments. A function with an arity of 2 must be provided
# with exactly two arguments, no more and no less. Added in Puppet 3.1.0.
#
# @option options [Puppet::Node::Environment] :environment (nil) can
# explicitly pass the environment we wanted the function added to. Only used
# to set logging functions in root environment
#
# @return [Hash] describing the function.
#
# @api public
def self.newfunction(name, options = {}, &block)
name = name.intern
environment = options[:environment] || Puppet.lookup(:current_environment)
Puppet.warning _("Overwriting previous definition for function %{name}") % { name: name } if get_function(name, environment)
arity = options[:arity] || -1
ftype = options[:type] || :statement
unless ftype == :statement or ftype == :rvalue
raise Puppet::DevError, _("Invalid statement type %{type}") % { type: ftype.inspect }
end
# the block must be installed as a method because it may use "return",
# which is not allowed from procs.
real_fname = "real_function_#{name}"
environment_module(environment).send(:define_method, real_fname, &block)
fname = "function_#{name}"
env_module = environment_module(environment)
env_module.send(:define_method, fname) do |*args|
Puppet::Util::Profiler.profile(_("Called %{name}") % { name: name }, [:functions, name]) do
if args[0].is_a? Array
if arity >= 0 and args[0].size != arity
raise ArgumentError, _("%{name}(): Wrong number of arguments given (%{arg_count} for %{arity})") % { name: name, arg_count: args[0].size, arity: arity }
elsif arity < 0 and args[0].size < (arity + 1).abs
raise ArgumentError, _("%{name}(): Wrong number of arguments given (%{arg_count} for minimum %{min_arg_count})") % { name: name, arg_count: args[0].size, min_arg_count: (arity + 1).abs }
end
r = Puppet::Pops::Evaluator::Runtime3FunctionArgumentConverter.convert_return(send(real_fname, args[0]))
# avoid leaking aribtrary value if not being an rvalue function
options[:type] == :rvalue ? r : nil
else
raise ArgumentError, _("custom functions must be called with a single array that contains the arguments. For example, function_example([1]) instead of function_example(1)")
end
end
end
func = { :arity => arity, :type => ftype, :name => fname }
func[:doc] = options[:doc] if options[:doc]
env_module.add_function_info(name, func)
func
end
# Determine if a function is defined
#
# @param [Symbol] name the function
# @param [Puppet::Node::Environment] environment the environment to find the function in
#
# @return [Symbol, false] The name of the function if it's defined,
# otherwise false.
#
# @api public
def self.function(name, environment = Puppet.lookup(:current_environment))
name = name.intern
func = get_function(name, environment)
unless func
autoloader.delegatee.load(name, environment)
func = get_function(name, environment)
end
if func
func[:name]
else
false
end
end
def self.functiondocs(environment = Puppet.lookup(:current_environment))
autoloader.delegatee.loadall(environment)
ret = ''.dup
merged_functions(environment).sort { |a, b| a[0].to_s <=> b[0].to_s }.each do |name, hash|
ret << "#{name}\n#{'-' * name.to_s.length}\n"
if hash[:doc]
ret << Puppet::Util::Docs.scrub(hash[:doc])
else
ret << "Undocumented.\n"
end
ret << "\n\n- *Type*: #{hash[:type]}\n\n"
end
ret
end
# Determine whether a given function returns a value.
#
# @param [Symbol] name the function
# @param [Puppet::Node::Environment] environment The environment to find the function in
# @return [Boolean] whether it is an rvalue function
#
# @api public
def self.rvalue?(name, environment = Puppet.lookup(:current_environment))
func = get_function(name, environment)
func ? func[:type] == :rvalue : false
end
# Return the number of arguments a function expects.
#
# @param [Symbol] name the function
# @param [Puppet::Node::Environment] environment The environment to find the function in
# @return [Integer] The arity of the function. See {newfunction} for
# the meaning of negative values.
#
# @api public
def self.arity(name, environment = Puppet.lookup(:current_environment))
func = get_function(name, environment)
func ? func[:arity] : -1
end
class << self
private
def merged_functions(environment)
root = environment_module(Puppet.lookup(:root_environment))
env = environment_module(environment)
root.all_function_info.merge(env.all_function_info)
end
def get_function(name, environment)
environment_module(environment).get_function_info(name.intern) || environment_module(Puppet.lookup(:root_environment)).get_function_info(name.intern)
end
end
class Error
def self.is4x(name)
raise Puppet::ParseError, _("%{name}() can only be called using the 4.x function API. See Scope#call_function") % { name: name }
end
end
end
|