bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb in bolt-1.29.1 vs bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb in bolt-1.30.0

- old
+ new

@@ -1,17 +1,20 @@ # 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. +# 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 no agent is detected on the target using the 'puppet_agent::version' task, it's installed -# using 'puppet_agent::install' and the puppet service is stopped/disabled using the 'service' task. +# 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. @@ -22,35 +25,54 @@ def script_compiler @script_compiler ||= Puppet::Pal::ScriptCompiler.new(closure_scope.compiler) end - def run_task(executor, targets, name, args = {}) + def inventory + Puppet.lookup(:bolt_inventory) + end + + def get_task(name, params = {}) tasksig = script_compiler.task_signature(name) raise Bolt::Error.new("#{name} could not be found", 'bolt/apply-prep') unless tasksig - task = Bolt::Task.new(tasksig.task_hash) - results = executor.run_task(targets, task, args) - raise Bolt::RunFailure.new(results, 'run_task', task.name) unless results.ok? - results + 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 + 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 = Puppet.lookup(:bolt_executor) - inventory = Puppet.lookup(:bolt_inventory) executor.report_function_call(self.class.name) targets = inventory.get_targets(target_spec) @@ -59,25 +81,51 @@ # 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 - versions = run_task(executor, unknown_targets, 'puppet_agent::version') + 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}" - inventory.set_feature(r.target, 'puppet-agent') + set_agent_feature(r.target) end unless need_install.empty? need_install_targets = need_install.map(&:target) - run_task(executor, need_install_targets, 'puppet_agent::install') - # Service task works best when targets have puppet-agent feature - need_install_targets.each { |target| inventory.set_feature(target, 'puppet-agent') } - # Ensure the Puppet service is stopped after new install - run_task(executor, need_install_targets, 'service', 'action' => 'stop', 'name' => 'puppet') - run_task(executor, need_install_targets, 'service', 'action' => 'disable', 'name' => 'puppet') + # 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') + hook = inventory.plugins.get_hook(opts['plugin'], :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| @@ -88,9 +136,10 @@ 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