bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb in bolt-3.14.1 vs bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb in bolt-3.15.0

- old
+ new

@@ -6,158 +6,191 @@ # Installs the `puppet-agent` package on targets if needed, then collects facts, # including any custom facts found in Bolt's module path. The package is # installed using either the configured plugin or the `task` plugin with the # `puppet_agent::install` task. # -# Agent installation 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. +# Agent installation 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. # # > **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. # @param options Options hash. + # @option options [Boolean] _catch_errors Whether to catch raised errors. # @option options [Array] _required_modules An array of modules to sync to the target. # @option options [String] _run_as User to run as using privilege escalation. - # @return [nil] + # @return [Bolt::ResultSet] # @example Prepare targets by name. # apply_prep('target1,target2') dispatch :apply_prep do param 'Boltlib::TargetSpec', :targets optional_param 'Hash[String, Data]', :options end - def script_compiler - @script_compiler ||= Puppet::Pal::ScriptCompiler.new(closure_scope.compiler) - end + def apply_prep(target_spec, options = {}) + unless Puppet[:tasks] + raise Puppet::ParseErrorWithIssue + .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'apply_prep') + end - def inventory - @inventory ||= Puppet.lookup(:bolt_inventory) - end + options = options.slice(*%w[_catch_errors _required_modules _run_as]) + targets = inventory.get_targets(target_spec) - 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 + executor.report_function_call(self.class.name) - 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")}" + executor.log_action('install puppet and gather facts', targets) do + executor.without_default_logging do + install_results = install_agents(targets, options) + facts_results = get_facts(install_results.ok_set.targets, options) + + Bolt::ResultSet.new(install_results.error_set.results + facts_results.results) + end end + end - Bolt::Task.from_task_signature(tasksig) + def applicator + @applicator ||= Puppet.lookup(:apply_executor) end - # rubocop:disable Naming/AccessorMethodName - def set_agent_feature(target) - inventory.set_feature(target, 'puppet-agent') + def executor + @executor ||= Puppet.lookup(:bolt_executor) end - # rubocop:enable Naming/AccessorMethodName + def inventory + @inventory ||= Puppet.lookup(:bolt_inventory) + end + + # Runs a task. This method is called by the puppet_library hook. + # def run_task(targets, task, args = {}, options = {}) executor.run_task(targets, task, args, options) end - # Returns true if the target has the puppet-agent feature defined, either from inventory or transport. - def agent?(target, executor, inventory) + # Returns true if the target has the puppet-agent feature defined, either from + # inventory or transport. + # + private def agent?(target) inventory.features(target).include?('puppet-agent') || - executor.transport(target.transport).provided_features.include?('puppet-agent') || target.remote? + 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, options = {}) - unless Puppet[:tasks] - raise Puppet::ParseErrorWithIssue - .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'apply_prep') - end - - # Unfreeze this - options = options.slice(*%w[_run_as _required_modules]) - - applicator = Puppet.lookup(:apply_executor) - - executor.report_function_call(self.class.name) - - targets = inventory.get_targets(target_spec) - - required_modules = options.delete('_required_modules').to_a + # Generate the plugin tarball. + # + private def build_plugin_tarball(required_modules) if required_modules.any? Puppet.debug("Syncing only required modules: #{required_modules.join(',')}.") end - # Gather facts, including custom facts - plugins = applicator.build_plugin_tarball do |mod| + tarball = applicator.build_plugin_tarball do |mod| next unless required_modules.empty? || required_modules.include?(mod.name) search_dirs = [] search_dirs << mod.plugins if mod.plugins? search_dirs << mod.pluginfacts if mod.pluginfacts? search_dirs end - 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, need_install_targets = targets.partition { |target| agent?(target, executor, inventory) } - agent_targets.each { |target| Puppet.debug "Puppet Agent feature declared for #{target.name}" } - unless need_install_targets.empty? - # lazy-load expensive gem code - require 'concurrent' - pool = Concurrent::ThreadPoolExecutor.new + Puppet::Pops::Types::PSensitiveType::Sensitive.new(tarball) + end - hooks = need_install_targets.map do |t| - opts = t.plugin_hooks&.fetch('puppet_library').dup - plugin_name = opts.delete('plugin') - hook = inventory.plugins.get_hook(plugin_name, :puppet_library) - # Give plan function options precedence over inventory options - { 'target' => t, - 'hook_proc' => hook.call(opts.merge(options), t, self) } - rescue StandardError => e - Bolt::Result.from_exception(t, e) - end + # Install the puppet-agent package on targets that need it. + # + private def install_agents(targets, options) + results = [] - hook_errors, ok_hooks = hooks.partition { |h| h.is_a?(Bolt::Result) } + agent_targets, agentless_targets = targets.partition { |target| agent?(target) } - futures = ok_hooks.map do |hash| - Concurrent::Future.execute(executor: pool) do - hash['hook_proc'].call - end - end + agent_targets.each do |target| + Puppet.debug("Puppet Agent feature declared for #{target}") + results << Bolt::Result.new(target) + 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 + unless agentless_targets.empty? + hooks, errors = get_hooks(agentless_targets, options) + hook_results = run_hooks(hooks) - need_install_targets.each { |target| set_agent_feature(target) } - end + hook_results.each do |result| + next unless result.ok? + inventory.set_feature(result.target, 'puppet-agent') + end - task = applicator.custom_facts_task - arguments = { 'plugins' => Puppet::Pops::Types::PSensitiveType::Sensitive.new(plugins) } - results = run_task(targets, task, arguments, options) + results.concat(hook_results).concat(errors) + end - # TODO: Standardize RunFailure type with error above - raise Bolt::RunFailure.new(results, 'run_task', task.name) unless results.ok? + Bolt::ResultSet.new(results).tap do |resultset| + unless resultset.ok? || options['_catch_errors'] + raise Bolt::RunFailure.new(resultset.error_set, 'apply_prep') + end + end + end - results.each do |result| - # Log a warning if the client version is < 6 - if unsupported_puppet?(result['clientversion']) - Bolt::Logger.deprecate( - "unsupported_puppet", - "Detected unsupported Puppet agent version #{result['clientversion']} on target "\ - "#{result.target}. Bolt supports Puppet agent 6.0.0 and higher." - ) - end + # Retrieve facts from each target and add them to inventory. + # + private def get_facts(targets, options) + return Bolt::ResultSet.new([]) unless targets.any? - inventory.add_facts(result.target, result.value) - end + task = applicator.custom_facts_task + args = { 'plugins' => build_plugin_tarball(options.delete('_required_modules').to_a) } + results = run_task(targets, task, args, options) + + unless results.ok? || options['_catch_errors'] + raise Bolt::RunFailure.new(results, 'run_task', task.name) + end + + results.each do |result| + next unless result.ok? + + if unsupported_puppet?(result['clientversion']) + Bolt::Logger.deprecate( + "unsupported_puppet", + "Detected unsupported Puppet agent version #{result['clientversion']} on target "\ + "#{result.target}. Bolt supports Puppet agent 6.0.0 and higher." + ) end + + inventory.add_facts(result.target, result.value) end - # Return nothing - nil + results + end + + # Return a list of targets and their puppet_library hooks. + # + private def get_hooks(targets, options) + hooks = [] + errors = [] + + targets.each do |target| + plugin_opts = target.plugin_hooks.fetch('puppet_library').dup + plugin_name = plugin_opts.delete('plugin') + hook = inventory.plugins.get_hook(plugin_name, :puppet_library) + + hooks << { 'target' => target, + 'proc' => hook.call(plugin_opts.merge(options), target, self) } + rescue StandardError => e + errors << Bolt::Result.from_exception(target, e) + end + + [hooks, errors] + end + + # Runs the puppet_library hook for each target, returning the result + # of each. + # + private def run_hooks(hooks) + require 'concurrent' + pool = Concurrent::ThreadPoolExecutor.new + + futures = hooks.map do |hook| + Concurrent::Future.execute(executor: pool) do + hook['proc'].call + end + end + + futures.zip(hooks).map do |future, hook| + future.value || Bolt::Result.from_exception(hook['target'], future.reason) + end end # Returns true if the client's major version is < 6. # private def unsupported_puppet?(client_version)