diff --git a/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb b/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb index c997d9e939..8f0bd1aa54 100644 --- a/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +++ b/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb @@ -38,15 +38,47 @@ def from_args(msg, kind = nil, details = nil, issue_code = nil) raise Puppet::ParseErrorWithIssue .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'fail_plan') end - + executor = Puppet.lookup(:bolt_executor) # Send Analytics Report executor.report_function_call(self.class.name) - + + # Process details to safely handle any Error objects within it + if details && details.is_a?(Hash) + sanitized_details = {} + details.each do |k, v| + # Handle both Bolt::Error and Puppet::DataTypes::Error objects + if v.is_a?(Puppet::DataTypes::Error) || v.is_a?(Bolt::Error) + # For Error objects, only include basic properties to prevent recursion + # Extract only essential information, avoiding any details hash + error_hash = { + 'kind' => v.respond_to?(:kind) ? v.kind : nil, + 'msg' => v.respond_to?(:msg) ? v.msg : v.message + } + # Add issue_code if it exists + error_hash['issue_code'] = v.issue_code if v.respond_to?(:issue_code) && v.issue_code + + # Clean up nil values + error_hash.compact! + + sanitized_details[k] = error_hash + else + sanitized_details[k] = v + end + end + details = sanitized_details + end + raise Bolt::PlanFailure.new(msg, kind || 'bolt/plan-failure', details, issue_code) end def from_error(err) - from_args(err.message, err.kind, err.details, err.issue_code) + # Extract just the basic properties + msg = err.message + kind = err.kind + issue_code = err.issue_code + + # Intentionally NOT passing err.details to avoid circular references + from_args(msg, kind, nil, issue_code) end end diff --git a/lib/bolt/error.rb b/lib/bolt/error.rb index 374121126e..998294afae 100644 --- a/lib/bolt/error.rb +++ b/lib/bolt/error.rb @@ -20,8 +20,25 @@ def msg def to_h h = { 'kind' => kind, - 'msg' => message, - 'details' => details } + 'msg' => message } + + # Process details with special handling for Error objects to prevent cycles + processed_details = {} + if details + details.each do |k, v| + if v.is_a?(Bolt::Error) + # For Error objects, only include basic properties to prevent recursion + processed_details[k] = { + 'kind' => v.kind, + 'msg' => v.message + } + else + processed_details[k] = v + end + end + end + + h['details'] = processed_details h['issue_code'] = issue_code if issue_code h end @@ -35,7 +52,11 @@ def to_json(opts = nil) end def to_puppet_error - Puppet::DataTypes::Error.from_asserted_hash(to_h) + # Create a minimal hash for conversion + h = { 'kind' => kind, 'msg' => message } + h['issue_code'] = issue_code if issue_code + + Puppet::DataTypes::Error.from_asserted_hash(h) end def self.unknown_task(task) @@ -130,8 +151,26 @@ def initialize(results, failed_indices) end class PlanFailure < Error - def initialize(*args) - super(*args) + def initialize(msg, kind = nil, details = nil, issue_code = nil) + # Process details to replace any Error objects with simple hashes + if details && details.is_a?(Hash) + safe_details = {} + details.each do |k, v| + if v.is_a?(Bolt::Error) + # Create a minimal representation of the error + safe_details[k] = { + 'kind' => v.kind, + 'msg' => v.message + } + safe_details[k]['issue_code'] = v.issue_code if v.issue_code + else + safe_details[k] = v + end + end + details = safe_details + end + + super(msg, kind || 'bolt/plan-failure', details, issue_code) @error_code = 2 end end diff --git a/lib/bolt/util.rb b/lib/bolt/util.rb index 56a4615fa7..9a1a4e9176 100644 --- a/lib/bolt/util.rb +++ b/lib/bolt/util.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'set' + module Bolt module Util class << self @@ -241,14 +243,22 @@ def walk_keys(data, &block) # Accepts a Data object and returns a copy with all hash and array values # Arrays and hashes including the initial object are modified before - # their descendants are. - def walk_vals(data, skip_top = false, &block) + # their descendants are. Includes cycle detection to prevent infinite recursion. + def walk_vals(data, skip_top = false, visited = Set.new, &block) + # Check if we've already visited this object to prevent infinite recursion + return "[CIRCULAR REFERENCE]" if visited.include?(data.object_id) + + # Only track objects that could cause cycles (complex objects) + if data.is_a?(Hash) || data.is_a?(Array) || data.is_a?(Bolt::Error) + visited = visited.add(data.object_id) + end + data = yield(data) unless skip_top case data when Hash - data.transform_values { |v| walk_vals(v, &block) } + data.transform_values { |v| walk_vals(v, false, visited, &block) } when Array - data.map { |v| walk_vals(v, &block) } + data.map { |v| walk_vals(v, false, visited, &block) } else data end @@ -256,22 +266,49 @@ def walk_vals(data, skip_top = false, &block) # Accepts a Data object and returns a copy with all hash and array values # modified by the given block. Descendants are modified before their - # parents. - def postwalk_vals(data, skip_top = false, &block) + # parents (post-order traversal). Includes cycle detection to prevent infinite recursion. + def postwalk_vals(data, skip_top = false, visited = Set.new, &block) + # Check if we've already visited this object to prevent infinite recursion + return "[CIRCULAR REFERENCE]" if visited.include?(data.object_id) + + # Only track objects that could cause cycles (complex objects) + if data.is_a?(Hash) || data.is_a?(Array) || data.is_a?(Bolt::Error) + visited = visited.add(data.object_id) + end + new_data = case data - when Hash - data.transform_values { |v| postwalk_vals(v, &block) } - when Array - data.map { |v| postwalk_vals(v, &block) } - else - data - end + when Hash + data.transform_values { |v| postwalk_vals(v, false, visited, &block) } + when Array + data.map { |v| postwalk_vals(v, false, visited, &block) } + else + data + end + if skip_top new_data else yield(new_data) end end + + # Safely converts any Bolt::Error objects in a data structure to simplified hashes + # to prevent circular references during serialization and deserialization + def sanitize_for_puppet(data) + postwalk_vals(data) do |value| + if value.is_a?(Bolt::Error) + # Create a simplified hash without any error objects in details + { + '_bolt_error' => true, + 'kind' => value.kind, + 'msg' => value.message, + 'issue_code' => value.issue_code + }.compact + else + value + end + end + end # Performs a deep_clone, using an identical copy if the cloned structure contains multiple # references to the same object and prevents endless recursion.