# frozen_string_literal: true # Adds additional resources to the catalog and relationship graph that are # generated by existing resources. There are two ways that a resource can # generate additional resources, either through the #generate method or the # #eval_generate method. # # @api private class Puppet::Transaction::AdditionalResourceGenerator attr_writer :relationship_graph # [boolean] true if any resource has attempted and failed to generate resources attr_reader :resources_failed_to_generate def initialize(catalog, relationship_graph, prioritizer) @catalog = catalog @relationship_graph = relationship_graph @prioritizer = prioritizer @resources_failed_to_generate = false end def generate_additional_resources(resource) return unless resource.respond_to?(:generate) begin generated = resource.generate rescue => detail @resources_failed_to_generate = true resource.log_exception(detail, _("Failed to generate additional resources using 'generate': %{detail}") % { detail: detail }) end return unless generated generated = [generated] unless generated.is_a?(Array) generated.collect! do |res| @catalog.resource(res.ref) || res end unless resource.depthfirst? # This is reversed because PUP-1963 changed how generated # resources were added to the catalog. It exists for backwards # compatibility only, and can probably be removed in Puppet 5 # # Previously, resources were given sequential priorities in the # relationship graph. Post-1963, resources are added to the # catalog one by one adjacent to the parent resource. This # causes an implicit reversal of their application order from # the old code. The reverse makes it all work like it did. generated.reverse! end generated.each do |res| add_resource(res, resource) add_generated_directed_dependency(resource, res) generate_additional_resources(res) end end def eval_generate(resource) return false unless resource.respond_to?(:eval_generate) raise Puppet::DevError, _("Depthfirst resources are not supported by eval_generate") if resource.depthfirst? begin generated = replace_duplicates_with_catalog_resources(resource.eval_generate) return false if generated.empty? rescue => detail @resources_failed_to_generate = true # TRANSLATORS eval_generate is a method name and should be left untranslated resource.log_exception(detail, _("Failed to generate additional resources using 'eval_generate': %{detail}") % { detail: detail }) return false end add_resources(generated, resource) made = generated.map(&:name).zip(generated).to_h contain_generated_resources_in(resource, made) connect_resources_to_ancestors(resource, made) true end private def replace_duplicates_with_catalog_resources(generated) generated.collect do |generated_resource| @catalog.resource(generated_resource.ref) || generated_resource end end def contain_generated_resources_in(resource, made) sentinel = Puppet::Type.type(:whit).new(:name => "completed_#{resource.title}", :catalog => resource.catalog) # Tag the completed whit with all of the tags of the generating resource # except the type name to allow event propogation to succeed beyond the whit # "boundary" when filtering resources with tags. We include auto-generated # tags such as the type name to support implicit filtering as well as # explicit. Note that resource#tags returns a duplicate of the resource's # tags. sentinel.merge_tags_from(resource) priority = @prioritizer.generate_priority_contained_in(resource, sentinel) @relationship_graph.add_vertex(sentinel, priority) redirect_edges_to_sentinel(resource, sentinel, made) made.values.each do |res| # This resource isn't 'completed' until each child has run add_conditional_directed_dependency(res, sentinel, Puppet::Graph::RelationshipGraph::Default_label) end # This edge allows the resource's events to propagate, though it isn't # strictly necessary for ordering purposes add_conditional_directed_dependency(resource, sentinel, Puppet::Graph::RelationshipGraph::Default_label) end def redirect_edges_to_sentinel(resource, sentinel, made) @relationship_graph.adjacent(resource, :direction => :out, :type => :edges).each do |e| next if made[e.target.name] @relationship_graph.add_relationship(sentinel, e.target, e.label) @relationship_graph.remove_edge! e end end def connect_resources_to_ancestors(resource, made) made.values.each do |res| # Depend on the nearest ancestor we generated, falling back to the # resource if we have none parent_name = res.ancestors.find { |a| made[a] and made[a] != res } parent = made[parent_name] || resource add_conditional_directed_dependency(parent, res) end end def add_resources(generated, resource) generated.each do |res| priority = @prioritizer.generate_priority_contained_in(resource, res) add_resource(res, resource, priority) end end def add_resource(res, parent_resource, priority = nil) if @catalog.resource(res.ref).nil? res.merge_tags_from(parent_resource) if parent_resource.depthfirst? @catalog.add_resource_before(parent_resource, res) else @catalog.add_resource_after(parent_resource, res) end @catalog.add_edge(@catalog.container_of(parent_resource), res) if @catalog.container_of(parent_resource) if @relationship_graph && priority # If we have a relationship_graph we should add the resource # to it (this is an eval_generate). If we don't, then the # relationship_graph has not yet been created and we can skip # adding it. @relationship_graph.add_vertex(res, priority) end res.finish end end # add correct edge for depth- or breadth- first traversal of # generated resource. Skip generating the edge if there is already # some sort of edge between the two resources. def add_generated_directed_dependency(parent, child, label = nil) if parent.depthfirst? source = child target = parent else source = parent target = child end # For each potential relationship metaparam, check if parent or # child references the other. If there are none, we should add our # edge. edge_exists = Puppet::Type.relationship_params.any? { |param| param_sym = param.name.to_sym if parent[param_sym] parent_contains = parent[param_sym].any? { |res| res.ref == child.ref } else parent_contains = false end if child[param_sym] child_contains = child[param_sym].any? { |res| res.ref == parent.ref } else child_contains = false end parent_contains || child_contains } unless edge_exists # We *cannot* use target.to_resource here! # # For reasons that are beyond my (and, perhaps, human) # comprehension, to_resource will call retrieve. This is # problematic if a generated resource needs the system to be # changed by a previous resource (think a file on a path # controlled by a mount resource). # # Instead of using to_resource, we just construct a resource as # if the arguments to the Type instance had been passed to a # Resource instead. resource = ::Puppet::Resource.new(target.class, target.title, :parameters => target.original_parameters) source[:before] ||= [] source[:before] << resource end end # Copy an important relationships from the parent to the newly-generated # child resource. def add_conditional_directed_dependency(parent, child, label = nil) @relationship_graph.add_vertex(child) edge = parent.depthfirst? ? [child, parent] : [parent, child] if @relationship_graph.edge?(*edge.reverse) parent.debug "Skipping automatic relationship to #{child}" else @relationship_graph.add_relationship(edge[0], edge[1], label) end end end