Skip to content

fix: addresses stack too deep error #3389

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
49 changes: 44 additions & 5 deletions lib/bolt/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
63 changes: 50 additions & 13 deletions lib/bolt/util.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'set'

module Bolt
module Util
class << self
Expand Down Expand Up @@ -241,37 +243,72 @@ 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
end

# 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.
Expand Down