Skip to content

Commit e8e56ab

Browse files
committed
fix: addresses the stack too deep error
1 parent b47e205 commit e8e56ab

File tree

3 files changed

+129
-21
lines changed

3 files changed

+129
-21
lines changed

bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,47 @@ def from_args(msg, kind = nil, details = nil, issue_code = nil)
3838
raise Puppet::ParseErrorWithIssue
3939
.from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'fail_plan')
4040
end
41-
41+
4242
executor = Puppet.lookup(:bolt_executor)
4343
# Send Analytics Report
4444
executor.report_function_call(self.class.name)
45-
45+
46+
# Process details to safely handle any Error objects within it
47+
if details && details.is_a?(Hash)
48+
sanitized_details = {}
49+
details.each do |k, v|
50+
# Handle both Bolt::Error and Puppet::DataTypes::Error objects
51+
if v.is_a?(Puppet::DataTypes::Error) || v.is_a?(Bolt::Error)
52+
# For Error objects, only include basic properties to prevent recursion
53+
# Extract only essential information, avoiding any details hash
54+
error_hash = {
55+
'kind' => v.respond_to?(:kind) ? v.kind : nil,
56+
'msg' => v.respond_to?(:msg) ? v.msg : v.message
57+
}
58+
# Add issue_code if it exists
59+
error_hash['issue_code'] = v.issue_code if v.respond_to?(:issue_code) && v.issue_code
60+
61+
# Clean up nil values
62+
error_hash.compact!
63+
64+
sanitized_details[k] = error_hash
65+
else
66+
sanitized_details[k] = v
67+
end
68+
end
69+
details = sanitized_details
70+
end
71+
4672
raise Bolt::PlanFailure.new(msg, kind || 'bolt/plan-failure', details, issue_code)
4773
end
4874

4975
def from_error(err)
50-
from_args(err.message, err.kind, err.details, err.issue_code)
76+
# Extract just the basic properties
77+
msg = err.message
78+
kind = err.kind
79+
issue_code = err.issue_code
80+
81+
# Intentionally NOT passing err.details to avoid circular references
82+
from_args(msg, kind, nil, issue_code)
5183
end
5284
end

lib/bolt/error.rb

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,25 @@ def msg
2020

2121
def to_h
2222
h = { 'kind' => kind,
23-
'msg' => message,
24-
'details' => details }
23+
'msg' => message }
24+
25+
# Process details with special handling for Error objects to prevent cycles
26+
processed_details = {}
27+
if details
28+
details.each do |k, v|
29+
if v.is_a?(Bolt::Error)
30+
# For Error objects, only include basic properties to prevent recursion
31+
processed_details[k] = {
32+
'kind' => v.kind,
33+
'msg' => v.message
34+
}
35+
else
36+
processed_details[k] = v
37+
end
38+
end
39+
end
40+
41+
h['details'] = processed_details
2542
h['issue_code'] = issue_code if issue_code
2643
h
2744
end
@@ -35,7 +52,11 @@ def to_json(opts = nil)
3552
end
3653

3754
def to_puppet_error
38-
Puppet::DataTypes::Error.from_asserted_hash(to_h)
55+
# Create a minimal hash for conversion
56+
h = { 'kind' => kind, 'msg' => message }
57+
h['issue_code'] = issue_code if issue_code
58+
59+
Puppet::DataTypes::Error.from_asserted_hash(h)
3960
end
4061

4162
def self.unknown_task(task)
@@ -130,8 +151,26 @@ def initialize(results, failed_indices)
130151
end
131152

132153
class PlanFailure < Error
133-
def initialize(*args)
134-
super(*args)
154+
def initialize(msg, kind = nil, details = nil, issue_code = nil)
155+
# Process details to replace any Error objects with simple hashes
156+
if details && details.is_a?(Hash)
157+
safe_details = {}
158+
details.each do |k, v|
159+
if v.is_a?(Bolt::Error)
160+
# Create a minimal representation of the error
161+
safe_details[k] = {
162+
'kind' => v.kind,
163+
'msg' => v.message
164+
}
165+
safe_details[k]['issue_code'] = v.issue_code if v.issue_code
166+
else
167+
safe_details[k] = v
168+
end
169+
end
170+
details = safe_details
171+
end
172+
173+
super(msg, kind || 'bolt/plan-failure', details, issue_code)
135174
@error_code = 2
136175
end
137176
end

lib/bolt/util.rb

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require 'set'
4+
35
module Bolt
46
module Util
57
class << self
@@ -241,37 +243,72 @@ def walk_keys(data, &block)
241243

242244
# Accepts a Data object and returns a copy with all hash and array values
243245
# Arrays and hashes including the initial object are modified before
244-
# their descendants are.
245-
def walk_vals(data, skip_top = false, &block)
246+
# their descendants are. Includes cycle detection to prevent infinite recursion.
247+
def walk_vals(data, skip_top = false, visited = Set.new, &block)
248+
# Check if we've already visited this object to prevent infinite recursion
249+
return "[CIRCULAR REFERENCE]" if visited.include?(data.object_id)
250+
251+
# Only track objects that could cause cycles (complex objects)
252+
if data.is_a?(Hash) || data.is_a?(Array) || data.is_a?(Bolt::Error)
253+
visited = visited.add(data.object_id)
254+
end
255+
246256
data = yield(data) unless skip_top
247257
case data
248258
when Hash
249-
data.transform_values { |v| walk_vals(v, &block) }
259+
data.transform_values { |v| walk_vals(v, false, visited, &block) }
250260
when Array
251-
data.map { |v| walk_vals(v, &block) }
261+
data.map { |v| walk_vals(v, false, visited, &block) }
252262
else
253263
data
254264
end
255265
end
256266

257267
# Accepts a Data object and returns a copy with all hash and array values
258268
# modified by the given block. Descendants are modified before their
259-
# parents.
260-
def postwalk_vals(data, skip_top = false, &block)
269+
# parents (post-order traversal). Includes cycle detection to prevent infinite recursion.
270+
def postwalk_vals(data, skip_top = false, visited = Set.new, &block)
271+
# Check if we've already visited this object to prevent infinite recursion
272+
return "[CIRCULAR REFERENCE]" if visited.include?(data.object_id)
273+
274+
# Only track objects that could cause cycles (complex objects)
275+
if data.is_a?(Hash) || data.is_a?(Array) || data.is_a?(Bolt::Error)
276+
visited = visited.add(data.object_id)
277+
end
278+
261279
new_data = case data
262-
when Hash
263-
data.transform_values { |v| postwalk_vals(v, &block) }
264-
when Array
265-
data.map { |v| postwalk_vals(v, &block) }
266-
else
267-
data
268-
end
280+
when Hash
281+
data.transform_values { |v| postwalk_vals(v, false, visited, &block) }
282+
when Array
283+
data.map { |v| postwalk_vals(v, false, visited, &block) }
284+
else
285+
data
286+
end
287+
269288
if skip_top
270289
new_data
271290
else
272291
yield(new_data)
273292
end
274293
end
294+
295+
# Safely converts any Bolt::Error objects in a data structure to simplified hashes
296+
# to prevent circular references during serialization and deserialization
297+
def sanitize_for_puppet(data)
298+
postwalk_vals(data) do |value|
299+
if value.is_a?(Bolt::Error)
300+
# Create a simplified hash without any error objects in details
301+
{
302+
'_bolt_error' => true,
303+
'kind' => value.kind,
304+
'msg' => value.message,
305+
'issue_code' => value.issue_code
306+
}.compact
307+
else
308+
value
309+
end
310+
end
311+
end
275312

276313
# Performs a deep_clone, using an identical copy if the cloned structure contains multiple
277314
# references to the same object and prevents endless recursion.

0 commit comments

Comments
 (0)