class Puppet::Parser::Compiler
Maintain a graph of scopes, along with a bunch of data about the individual catalog we're compiling.
Abstract class for a catalog validator that can be registered with the compiler to run at a certain stage.
Constants
- SETTINGS
Attributes
The id of code input to the compiler. @api private
Access to the configured loaders for 4x @return [Puppet::Pops::Loader::Loaders] the configured loaders @api private
Public Class Methods
# File lib/puppet/parser/compiler.rb 22 def self.compile(node, code_id = nil) 23 node.environment.check_for_reparse 24 25 errors = node.environment.validation_errors 26 if !errors.empty? 27 errors.each { |e| Puppet.err(e) } if errors.size > 1 28 errmsg = [ 29 _("Compilation has been halted because: %{error}") % { error: errors.first }, 30 _("For more information, see https://puppet.com/docs/puppet/latest/environments_about.html"), 31 ] 32 raise(Puppet::Error, errmsg.join(' ')) 33 end 34 35 new(node, :code_id => code_id).compile {|resulting_catalog| resulting_catalog.to_resource } 36 rescue Puppet::ParseErrorWithIssue => detail 37 detail.node = node.name 38 Puppet.log_exception(detail) 39 raise 40 rescue => detail 41 message = _("%{message} on node %{node}") % { message: detail, node: node.name } 42 Puppet.log_exception(detail, message) 43 raise Puppet::Error, message, detail.backtrace 44 end
# File lib/puppet/parser/compiler.rb 274 def initialize(node, code_id: nil) 275 @node = sanitize_node(node) 276 @code_id = code_id 277 initvars 278 add_catalog_validators 279 # Resolutions of fully qualified variable names 280 @qualified_variables = {} 281 end
Public Instance Methods
Add a catalog validator that will run at some stage to this compiler @param catalog_validators [Class<CatalogValidator>] The catalog validator class to add
# File lib/puppet/parser/compiler.rb 105 def add_catalog_validator(catalog_validators) 106 @catalog_validators << catalog_validators 107 nil 108 end
# File lib/puppet/parser/compiler.rb 110 def add_catalog_validators 111 add_catalog_validator(CatalogValidator::RelationshipValidator) 112 end
Store the fact that we've evaluated a class
# File lib/puppet/parser/compiler.rb 99 def add_class(name) 100 @catalog.add_class(name) unless name == "" 101 end
Store a resource override.
# File lib/puppet/parser/compiler.rb 63 def add_override(override) 64 # If possible, merge the override in immediately. 65 resource = @catalog.resource(override.ref) 66 if resource 67 resource.merge(override) 68 else 69 # Otherwise, store the override for later; these 70 # get evaluated in Resource#finish. 71 @resource_overrides[override.ref] << override 72 end 73 end
# File lib/puppet/parser/compiler.rb 75 def add_resource(scope, resource) 76 @resources << resource 77 78 # Note that this will fail if the resource is not unique. 79 @catalog.add_resource(resource) 80 81 if not resource.class? and resource[:stage] 82 #TRANSLATORS "stage" is a keyword in Puppet and should not be translated 83 raise ArgumentError, _("Only classes can set 'stage'; normal resources like %{resource} cannot change run stage") % { resource: resource } 84 end 85 86 # Stages should not be inside of classes. They are always a 87 # top-level container, regardless of where they appear in the 88 # manifest. 89 return if resource.stage? 90 91 # This adds a resource to the class it lexically appears in in the 92 # manifest. 93 unless resource.class? 94 @catalog.add_edge(scope.resource, resource) 95 end 96 end
Compiler our catalog. This mostly revolves around finding and evaluating classes. This is the main entry into our catalog.
# File lib/puppet/parser/compiler.rb 123 def compile 124 Puppet.override( @context_overrides , _("For compiling %{node}") % { node: node.name }) do 125 @catalog.environment_instance = environment 126 127 # Set the client's parameters into the top scope. 128 Puppet::Util::Profiler.profile(_("Compile: Set node parameters"), [:compiler, :set_node_params]) { set_node_parameters } 129 130 Puppet::Util::Profiler.profile(_("Compile: Created settings scope"), [:compiler, :create_settings_scope]) { create_settings_scope } 131 132 #TRANSLATORS "main" is a function name and should not be translated 133 Puppet::Util::Profiler.profile(_("Compile: Evaluated main"), [:compiler, :evaluate_main]) { evaluate_main } 134 135 Puppet::Util::Profiler.profile(_("Compile: Evaluated AST node"), [:compiler, :evaluate_ast_node]) { evaluate_ast_node } 136 137 Puppet::Util::Profiler.profile(_("Compile: Evaluated node classes"), [:compiler, :evaluate_node_classes]) { evaluate_node_classes } 138 139 Puppet::Util::Profiler.profile(_("Compile: Evaluated generators"), [:compiler, :evaluate_generators]) { evaluate_generators } 140 141 Puppet::Util::Profiler.profile(_("Compile: Validate Catalog pre-finish"), [:compiler, :validate_pre_finish]) do 142 validate_catalog(CatalogValidator::PRE_FINISH) 143 end 144 145 Puppet::Util::Profiler.profile(_("Compile: Finished catalog"), [:compiler, :finish_catalog]) { finish } 146 147 fail_on_unevaluated 148 149 Puppet::Util::Profiler.profile(_("Compile: Validate Catalog final"), [:compiler, :validate_final]) do 150 validate_catalog(CatalogValidator::FINAL) 151 end 152 153 if block_given? 154 yield @catalog 155 else 156 @catalog 157 end 158 end 159 end
Constructs the overrides for the context
# File lib/puppet/parser/compiler.rb 166 def context_overrides() 167 { 168 :current_environment => environment, 169 :global_scope => @topscope, # 4x placeholder for new global scope 170 :loaders => @loaders, # 4x loaders 171 } 172 end
Return the node's environment.
# File lib/puppet/parser/compiler.rb 177 def environment 178 node.environment 179 end
If ast nodes are enabled, then see if we can find and evaluate one.
@api private
# File lib/puppet/parser/compiler.rb 207 def evaluate_ast_node 208 krt = environment.known_resource_types 209 return unless krt.nodes? #ast_nodes? 210 211 # Now see if we can find the node. 212 astnode = nil 213 @node.names.each do |name| 214 astnode = krt.node(name.to_s.downcase) 215 break if astnode 216 end 217 218 unless (astnode ||= krt.node("default")) 219 raise Puppet::ParseError, _("Could not find node statement with name 'default' or '%{names}'") % { names: node.names.join(", ") } 220 end 221 222 # Create a resource to model this node, and then add it to the list 223 # of resources. 224 resource = astnode.ensure_in_catalog(topscope) 225 226 resource.evaluate 227 228 @node_scope = topscope.class_scope(astnode) 229 end
Evaluates each specified class in turn. If there are any classes that can't be found, an error is raised. This method really just creates resource objects that point back to the classes, and then the resources are themselves evaluated later in the process.
# File lib/puppet/parser/compiler.rb 236 def evaluate_classes(classes, scope, lazy_evaluate = true) 237 raise Puppet::DevError, _("No source for scope passed to evaluate_classes") unless scope.source 238 class_parameters = nil 239 # if we are a param class, save the classes hash 240 # and transform classes to be the keys 241 if classes.class == Hash 242 class_parameters = classes 243 classes = classes.keys 244 end 245 246 hostclasses = classes.collect do |name| 247 environment.known_resource_types.find_hostclass(name) or raise Puppet::Error, _("Could not find class %{name} for %{node}") % { name: name, node: node.name } 248 end 249 250 if class_parameters 251 resources = ensure_classes_with_parameters(scope, hostclasses, class_parameters) 252 if !lazy_evaluate 253 resources.each(&:evaluate) 254 end 255 256 resources 257 else 258 already_included, newly_included = ensure_classes_without_parameters(scope, hostclasses) 259 if !lazy_evaluate 260 newly_included.each(&:evaluate) 261 end 262 263 already_included + newly_included 264 end 265 end
Evaluate all of the classes specified by the node. Classes with parameters are evaluated as if they were declared. Classes without parameters or with an empty set of parameters are evaluated as if they were included. This means classes with an empty set of parameters won't conflict even if the class has already been included.
# File lib/puppet/parser/compiler.rb 186 def evaluate_node_classes 187 if @node.classes.is_a? Hash 188 classes_with_params, classes_without_params = @node.classes.partition {|name,params| params and !params.empty?} 189 190 # The results from Hash#partition are arrays of pairs rather than hashes, 191 # so we have to convert to the forms evaluate_classes expects (Hash, and 192 # Array of class names) 193 classes_with_params = Hash[classes_with_params] 194 classes_without_params.map!(&:first) 195 else 196 classes_with_params = {} 197 classes_without_params = @node.classes 198 end 199 200 evaluate_classes(classes_with_params, @node_scope || topscope) 201 evaluate_classes(classes_without_params, @node_scope || topscope) 202 end
# File lib/puppet/parser/compiler.rb 267 def evaluate_relationships 268 @relationships.each { |rel| rel.evaluate(catalog) } 269 end
Create a new scope, with either a specified parent scope or using the top scope.
# File lib/puppet/parser/compiler.rb 285 def newscope(parent, options = {}) 286 parent ||= topscope 287 scope = Puppet::Parser::Scope.new(self, **options) 288 scope.parent = parent 289 scope 290 end
Return any overrides for the given resource.
# File lib/puppet/parser/compiler.rb 293 def resource_overrides(resource) 294 @resource_overrides[resource.ref] 295 end
# File lib/puppet/parser/compiler.rb 161 def validate_catalog(validation_stage) 162 @catalog_validators.select { |vclass| vclass.validation_stage?(validation_stage) }.each { |vclass| vclass.new(@catalog).validate } 163 end
# File lib/puppet/parser/compiler.rb 117 def with_context_overrides(description = '', &block) 118 Puppet.override( @context_overrides , description, &block) 119 end
Protected Instance Methods
Iterate over collections and resources until we're sure that the whole compile is evaluated. This is necessary because both collections and defined resources can generate new resources, which themselves could be defined resources.
# File lib/puppet/parser/compiler.rb 368 def evaluate_generators 369 count = 0 370 loop do 371 done = true 372 373 Puppet::Util::Profiler.profile(_("Iterated (%{count}) on generators") % { count: count + 1 }, [:compiler, :iterate_on_generators]) do 374 # Call collections first, then definitions. 375 done = false if evaluate_collections 376 done = false if evaluate_definitions 377 end 378 379 break if done 380 381 count += 1 382 383 if count > 1000 384 raise Puppet::ParseError, _("Somehow looped more than 1000 times while evaluating host catalog") 385 end 386 end 387 end
Make sure all of our resources and such have done any last work necessary.
# File lib/puppet/parser/compiler.rb 433 def finish 434 evaluate_relationships 435 436 resources.each do |resource| 437 # Add in any resource overrides. 438 overrides = resource_overrides(resource) 439 if overrides 440 overrides.each do |over| 441 resource.merge(over) 442 end 443 444 # Remove the overrides, so that the configuration knows there 445 # are none left. 446 overrides.clear 447 end 448 449 resource.finish if resource.respond_to?(:finish) 450 end 451 452 add_resource_metaparams 453 end
Private Instance Methods
# File lib/puppet/parser/compiler.rb 456 def add_resource_metaparams 457 main = catalog.resource(:class, :main) 458 unless main 459 #TRANSLATORS "main" is a function name and should not be translated 460 raise _("Couldn't find main") 461 end 462 463 names = Puppet::Type.metaparams.select do |name| 464 !Puppet::Parser::Resource.relationship_parameter?(name) 465 end 466 data = {} 467 catalog.walk(main, :out) do |source, target| 468 source_data = data[source] || metaparams_as_data(source, names) 469 if source_data 470 # only store anything in the data hash if we've actually got 471 # data 472 data[source] ||= source_data 473 source_data.each do |param, value| 474 target[param] = value if target[param].nil? 475 end 476 data[target] = source_data.merge(metaparams_as_data(target, names)) 477 end 478 479 target.merge_tags_from(source) 480 end 481 end
# File lib/puppet/parser/compiler.rb 585 def create_settings_scope 586 settings_type = create_settings_type 587 settings_resource = Puppet::Parser::Resource.new('class', SETTINGS, :scope => @topscope) 588 589 @catalog.add_resource(settings_resource) 590 settings_type.evaluate_code(settings_resource) 591 settings_resource.instance_variable_set(:@evaluated, true) # Prevents settings from being reevaluated 592 593 scope = @topscope.class_scope(settings_type) 594 scope.merge_settings(environment.name) 595 end
# File lib/puppet/parser/compiler.rb 597 def create_settings_type 598 environment.lock.synchronize do 599 resource_types = environment.known_resource_types 600 settings_type = resource_types.hostclass(SETTINGS) 601 if settings_type.nil? 602 settings_type = Puppet::Resource::Type.new(:hostclass, SETTINGS) 603 resource_types.add(settings_type) 604 end 605 606 settings_type 607 end 608 end
# File lib/puppet/parser/compiler.rb 299 def ensure_classes_with_parameters(scope, hostclasses, parameters) 300 hostclasses.collect do |klass| 301 klass.ensure_in_catalog(scope, parameters[klass.name] || {}) 302 end 303 end
# File lib/puppet/parser/compiler.rb 305 def ensure_classes_without_parameters(scope, hostclasses) 306 already_included = [] 307 newly_included = [] 308 hostclasses.each do |klass| 309 class_scope = scope.class_scope(klass) 310 if class_scope 311 already_included << class_scope.resource 312 else 313 newly_included << klass.ensure_in_catalog(scope) 314 end 315 end 316 317 [already_included, newly_included] 318 end
Evaluate our collections and return true if anything returned an object. The 'true' is used to continue a loop, so it's important.
# File lib/puppet/parser/compiler.rb 322 def evaluate_collections 323 return false if @collections.empty? 324 325 exceptwrap do 326 # We have to iterate over a dup of the array because 327 # collections can delete themselves from the list, which 328 # changes its length and causes some collections to get missed. 329 Puppet::Util::Profiler.profile(_("Evaluated collections"), [:compiler, :evaluate_collections]) do 330 found_something = false 331 @collections.dup.each do |collection| 332 found_something = true if collection.evaluate 333 end 334 found_something 335 end 336 end 337 end
Make sure all of our resources have been evaluated into native resources. We return true if any resources have, so that we know to continue the evaluate_generators loop.
# File lib/puppet/parser/compiler.rb 342 def evaluate_definitions 343 exceptwrap do 344 Puppet::Util::Profiler.profile(_("Evaluated definitions"), [:compiler, :evaluate_definitions]) do 345 urs = unevaluated_resources.each do |resource| 346 begin 347 resource.evaluate 348 rescue Puppet::Pops::Evaluator::PuppetStopIteration => detail 349 # needs to be handled specifically as the error has the file/line/position where this 350 # occurred rather than the resource 351 fail(Puppet::Pops::Issues::RUNTIME_ERROR, detail, {:detail => detail.message}, detail) 352 353 rescue Puppet::Error => e 354 # PuppetError has the ability to wrap an exception, if so, use the wrapped exception's 355 # call stack instead 356 fail(Puppet::Pops::Issues::RUNTIME_ERROR, resource, {:detail => e.message}, e.original || e) 357 end 358 end 359 !urs.empty? 360 end 361 end 362 end
Find and evaluate our main object, if possible.
# File lib/puppet/parser/compiler.rb 391 def evaluate_main 392 krt = environment.known_resource_types 393 @main = krt.find_hostclass('') || krt.add(Puppet::Resource::Type.new(:hostclass, '')) 394 @topscope.source = @main 395 @main_resource = Puppet::Parser::Resource.new('class', :main, :scope => @topscope, :source => @main) 396 @topscope.resource = @main_resource 397 398 add_resource(@topscope, @main_resource) 399 400 @main_resource.evaluate 401 end
Make sure the entire catalog is evaluated.
# File lib/puppet/parser/compiler.rb 404 def fail_on_unevaluated 405 fail_on_unevaluated_overrides 406 fail_on_unevaluated_resource_collections 407 end
If there are any resource overrides remaining, then we could not find the resource they were supposed to override, so we want to throw an exception.
# File lib/puppet/parser/compiler.rb 412 def fail_on_unevaluated_overrides 413 remaining = @resource_overrides.values.flatten.collect(&:ref) 414 415 if !remaining.empty? 416 raise Puppet::ParseError, _("Could not find resource(s) %{resources} for overriding") % { resources: remaining.join(', ') } 417 end 418 end
Make sure there are no remaining collections that are waiting for resources that have not yet been instantiated. If this occurs it is an error (missing resource - it could not be realized).
# File lib/puppet/parser/compiler.rb 424 def fail_on_unevaluated_resource_collections 425 remaining = @collections.collect(&:unresolved_resources).flatten.compact 426 if !remaining.empty? 427 raise Puppet::ParseError, _("Failed to realize virtual resources %{resources}") % { resources: remaining.join(', ') } 428 end 429 end
Set up all of our internal variables.
# File lib/puppet/parser/compiler.rb 499 def initvars 500 # The list of overrides. This is used to cache overrides on objects 501 # that don't exist yet. We store an array of each override. 502 @resource_overrides = Hash.new do |overs, ref| 503 overs[ref] = [] 504 end 505 506 # The list of collections that have been created. This is a global list, 507 # but they each refer back to the scope that created them. 508 @collections = [] 509 510 # The list of relationships to evaluate. 511 @relationships = [] 512 513 # For maintaining the relationship between scopes and their resources. 514 @catalog = Puppet::Resource::Catalog.new(@node.name, @node.environment, @code_id) 515 516 # MOVED HERE - SCOPE IS NEEDED (MOVE-SCOPE) 517 # Create the initial scope, it is needed early 518 @topscope = Puppet::Parser::Scope.new(self) 519 520 # Initialize loaders and Pcore 521 @loaders = Puppet::Pops::Loaders.new(environment) 522 523 # Need to compute overrides here, and remember them, because we are about to 524 # enter the magic zone of known_resource_types and initial import. 525 # Expensive entries in the context are bound lazily. 526 @context_overrides = context_overrides() 527 528 # This construct ensures that initial import (triggered by instantiating 529 # the structure 'known_resource_types') has a configured context 530 # It cannot survive the initvars method, and is later reinstated 531 # as part of compiling... 532 # 533 Puppet.override( @context_overrides , _("For initializing compiler")) do 534 # THE MAGIC STARTS HERE ! This triggers parsing, loading etc. 535 @catalog.version = environment.known_resource_types.version 536 @loaders.pre_load 537 end 538 539 @catalog.add_resource(Puppet::Parser::Resource.new("stage", :main, :scope => @topscope)) 540 541 # local resource array to maintain resource ordering 542 @resources = [] 543 544 # Make sure any external node classes are in our class list 545 if @node.classes.class == Hash 546 @catalog.add_class(*@node.classes.keys) 547 else 548 @catalog.add_class(*@node.classes) 549 end 550 551 @catalog_validators = [] 552 end
# File lib/puppet/parser/compiler.rb 483 def metaparams_as_data(resource, params) 484 data = nil 485 params.each do |param| 486 unless resource[param].nil? 487 # Because we could be creating a hash for every resource, 488 # and we actually probably don't often have any data here at all, 489 # we're optimizing a bit by only creating a hash if there's 490 # any data to put in it. 491 data ||= {} 492 data[param] = resource[param] 493 end 494 end 495 data 496 end
# File lib/puppet/parser/compiler.rb 554 def sanitize_node(node) 555 node.sanitize 556 node 557 end
Set the node's parameters into the top-scope as variables.
# File lib/puppet/parser/compiler.rb 560 def set_node_parameters 561 node.parameters.each do |param, value| 562 # We don't want to set @topscope['environment'] from the parameters, 563 # instead we want to get that from the node's environment itself in 564 # case a custom node terminus has done any mucking about with 565 # node.parameters. 566 next if param.to_s == 'environment' 567 # Ensure node does not leak Symbol instances in general 568 @topscope[param.to_s] = value.is_a?(Symbol) ? value.to_s : value 569 end 570 @topscope['environment'] = node.environment.name.to_s 571 572 # These might be nil. 573 catalog.client_version = node.parameters["clientversion"] 574 catalog.server_version = node.parameters["serverversion"] 575 @topscope.set_trusted(node.trusted_data) 576 577 @topscope.set_server_facts(node.server_facts) 578 579 facts_hash = node.facts.nil? ? {} : node.facts.values 580 @topscope.set_facts(facts_hash) 581 end
Return an array of all of the unevaluated resources. These will be definitions, which need to get evaluated into native resources.
# File lib/puppet/parser/compiler.rb 612 def unevaluated_resources 613 # The order of these is significant for speed due to short-circuiting 614 resources.reject { |resource| resource.evaluated? or resource.virtual? or resource.builtin_type? } 615 end