# frozen_string_literal: true require 'bolt/logger' require 'bolt/task' # 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. # # > **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 [Array] _required_modules An array of modules to sync to the target. # @return [nil] # @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 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.from_task_signature(tasksig) 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 = {}, 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) 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, options = {}) unless Puppet[:tasks] raise Puppet::ParseErrorWithIssue .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'apply_prep') end options = options.transform_keys { |k| k.sub(/^_/, '').to_sym } applicator = Puppet.lookup(:apply_executor) executor.report_function_call(self.class.name) targets = inventory.get_targets(target_spec) required_modules = options[:required_modules].nil? ? nil : Array(options[: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| next unless required_modules.nil? || 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 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) { 'target' => t, 'hook_proc' => hook.call(opts, t, self) } rescue StandardError => e Bolt::Result.from_exception(t, e) 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 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| # 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 inventory.add_facts(result.target, result.value) end end end # Return nothing nil end # Returns true if the client's major version is < 6. # private def unsupported_puppet?(client_version) if client_version.nil? false else begin Integer(client_version.split('.').first) < 6 rescue StandardError false end end end end