# frozen_string_literal: true require 'bolt/task' # Installs the puppet-agent package on targets if needed, then collects facts, # including any custom facts found in Bolt's modulepath. The package is # installed using either the configured plugin or the `task` plugin with the # `puppet_agent::install` task. # # Agent detection will be skipped if the target includes the 'puppet-agent' feature, either as a # property of its transport (PCP) or by explicitly setting it as a feature in Bolt's inventory. # # If Bolt does not detect an agent on the target using the 'puppet_agent::version' task, # it will install the agent using either the configured plugin or the # task plugin. # # **NOTE:** Not available in apply block Puppet::Functions.create_function(:apply_prep) do # @param targets A pattern or array of patterns identifying a set of targets. # @example Prepare targets by name. # apply_prep('target1,target2') dispatch :apply_prep do param 'Boltlib::TargetSpec', :targets end def script_compiler @script_compiler ||= Puppet::Pal::ScriptCompiler.new(closure_scope.compiler) end def inventory @inventory ||= Puppet.lookup(:bolt_inventory) end def get_task(name, params = {}) tasksig = script_compiler.task_signature(name) raise Bolt::Error.new("Task '#{name}' could not be found", 'bolt/apply-prep') unless tasksig errors = [] unless tasksig.runnable_with?(params) { |msg| errors << msg } # This relies on runnable with printing a partial message before the first real error raise Bolt::ValidationError, "Invalid parameters for #{errors.join("\n")}" end Bolt::Task.new(tasksig.task_hash) end # rubocop:disable Naming/AccessorMethodName def set_agent_feature(target) inventory.set_feature(target, 'puppet-agent') end # rubocop:enable Naming/AccessorMethodName def run_task(targets, task, args = {}) executor.run_task(targets, task, args) end # Returns true if the target has the puppet-agent feature defined, either from inventory or transport. def agent?(target, executor, inventory) inventory.features(target).include?('puppet-agent') || executor.transport(target.transport).provided_features.include?('puppet-agent') || target.remote? end def executor @executor ||= Puppet.lookup(:bolt_executor) end def apply_prep(target_spec) unless Puppet[:tasks] raise Puppet::ParseErrorWithIssue .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'apply_prep') end applicator = Puppet.lookup(:apply_executor) executor.report_function_call(self.class.name) targets = inventory.get_targets(target_spec) executor.log_action('install puppet and gather facts', targets) do executor.without_default_logging do # Skip targets that include the puppet-agent feature, as we know an agent will be available. agent_targets, unknown_targets = targets.partition { |target| agent?(target, executor, inventory) } agent_targets.each { |target| Puppet.debug "Puppet Agent feature declared for #{target.name}" } unless unknown_targets.empty? # Ensure Puppet is installed version_task = get_task('puppet_agent::version') versions = run_task(unknown_targets, version_task) raise Bolt::RunFailure.new(versions, 'run_task', 'puppet_agent::version') unless versions.ok? need_install, installed = versions.partition { |r| r['version'].nil? } installed.each do |r| Puppet.debug "Puppet Agent #{r['version']} installed on #{r.target.name}" set_agent_feature(r.target) end unless need_install.empty? need_install_targets = need_install.map(&:target) # lazy-load expensive gem code require 'concurrent' pool = Concurrent::ThreadPoolExecutor.new hooks = need_install_targets.map do |t| begin opts = t.plugin_hooks&.fetch('puppet_library').dup plugin_name = opts.delete('plugin') hook = inventory.plugins.get_hook(plugin_name, :puppet_library) { 'target' => t, 'hook_proc' => hook.call(opts, t, self) } rescue StandardError => e Bolt::Result.from_exception(t, e) end end hook_errors, ok_hooks = hooks.partition { |h| h.is_a?(Bolt::Result) } futures = ok_hooks.map do |hash| Concurrent::Future.execute(executor: pool) do hash['hook_proc'].call end end results = futures.zip(ok_hooks).map do |f, hash| f.value || Bolt::Result.from_exception(hash['target'], f.reason) end set = Bolt::ResultSet.new(results + hook_errors) raise Bolt::RunFailure.new(set.error_set, 'apply_prep') unless set.ok need_install_targets.each { |target| set_agent_feature(target) } end end # Gather facts, including custom facts plugins = applicator.build_plugin_tarball do |mod| search_dirs = [] search_dirs << mod.plugins if mod.plugins? search_dirs << mod.pluginfacts if mod.pluginfacts? search_dirs end task = applicator.custom_facts_task arguments = { 'plugins' => Puppet::Pops::Types::PSensitiveType::Sensitive.new(plugins) } results = executor.run_task(targets, task, arguments) # TODO: Standardize RunFailure type with error above raise Bolt::RunFailure.new(results, 'run_task', task.name) unless results.ok? results.each do |result| inventory.add_facts(result.target, result.value) end end end # Return nothing nil end end