1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
|
#
# Copyright:: Copyright 2018-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require_relative "event_dispatch/base"
class Chef
class ActionCollection < EventDispatch::Base
include Enumerable
extend Forwardable
class ActionRecord
# XXX: this is buggy since we (ab)use this resource for "after" state and it may be
# inaccurate and it may be mutated by the user. A third after_resource should be added
# to new_resource + current_resource to properly implement this.
#
# @return [Chef::Resource] The declared resource state.
#
attr_accessor :new_resource
# @return [Chef::Resource] The current_resource object (before-state). This can be nil
# for non-why-run-safe resources in why-run mode, or if load_current_resource itself
# threw an exception (which should be considered a bug in that load_current_resource
# implementation, but must be handled), or for unprocessed resources.
attr_accessor :current_resource
# @return [Symbol] # The action that was run (or scheduled to run in the case of "unprocessed" resources).
attr_accessor :action
# @return [Exception] The exception that was thrown
attr_accessor :exception
# @return [Numeric] The elapsed time in seconds with machine precision
attr_accessor :elapsed_time
# @return [Chef::Resource::Conditional] The conditional that caused the resource to be skipped
attr_accessor :conditional
# The status of the resource:
# - updated: ran and converged
# - up_to_date: skipped due to idempotency
# - skipped: skipped due to a conditional
# - failed: failed with an exception
# - unprocessed: resources that were not touched by a run that failed
#
# @return [Symbol] status
#
attr_accessor :status
# The "nesting" level. Outer resources in recipe context are 0 here, while for every
# sub-resource_collection inside of a custom resource this number is incremented by 1.
# Resources that are fired via build-resource or manually creating and firing
#
# @return [Integer]
#
attr_accessor :nesting_level
def initialize(new_resource, action, nesting_level)
@new_resource = new_resource
@action = action
@nesting_level = nesting_level
end
# @return [Boolean] true if there was no exception
def success?
!exception
end
end
attr_reader :action_records
attr_reader :pending_updates
attr_reader :run_context
attr_reader :consumers
attr_reader :events
def initialize(events, run_context = nil, action_records = [])
@action_records = action_records
@pending_updates = []
@consumers = []
@events = events
@run_context = run_context
end
def_delegators :@action_records, :each, :last
# Allows getting at the action_records collection filtered by nesting level and status.
#
# TODO: filtering by resource type+name
#
# @return [Chef::ActionCollection]
#
def filtered_collection(max_nesting: nil, up_to_date: true, skipped: true, updated: true, failed: true, unprocessed: true)
subrecords = action_records.select do |rec|
( max_nesting.nil? || rec.nesting_level <= max_nesting ) &&
( rec.status == :up_to_date && up_to_date ||
rec.status == :skipped && skipped ||
rec.status == :updated && updated ||
rec.status == :failed && failed ||
rec.status == :unprocessed && unprocessed )
end
self.class.new(events, run_context, subrecords)
end
# This hook gives us the run_context immediately after it is created so that we can wire up this object to it.
#
# This also causes the action_collection_registration event to fire, all consumers that have not yet registered with the
# action_collection must register via this callback. This is the latest point before resources actually start to get
# evaluated.
#
# (see EventDispatch::Base#)
#
def cookbook_compilation_start(run_context)
run_context.action_collection = self
# fire the action_colleciton_registration hook after cookbook_compilation_start -- last chance for consumers to register
run_context.events.enqueue(:action_collection_registration, self)
@run_context = run_context
end
# Consumers must call register -- either directly or through the action_collection_registration hook. If
# nobody has registered any interest, then no action tracking will be done.
#
# @params object [Object] callers should call with `self`
#
def register(object)
consumers << object
end
# End of an unsuccessful converge used to fire off detect_unprocessed_resources.
#
# (see EventDispatch::Base#)
#
def converge_failed(exception)
return if consumers.empty?
detect_unprocessed_resources
end
# Hook to start processing a resource. May be called within processing of an outer resource
# so the pending_updates array forms a stack that sub-resources are popped onto and off of.
# This is always called.
#
# (see EventDispatch::Base#)
#
def resource_action_start(new_resource, action, notification_type = nil, notifier = nil)
return if consumers.empty?
pending_updates << ActionRecord.new(new_resource, action, pending_updates.length)
end
# Hook called after a resource is loaded. If load_current_resource fails, this hook will
# not be called and current_resource will be nil, and the resource_failed hook will be called.
#
# (see EventDispatch::Base#)
#
def resource_current_state_loaded(new_resource, action, current_resource)
return if consumers.empty?
current_record.current_resource = current_resource
end
# Hook called after an action is determined to be up to date.
#
# (see EventDispatch::Base#)
#
def resource_up_to_date(new_resource, action)
return if consumers.empty?
current_record.status = :up_to_date
end
# Hook called after an action is determined to be skipped due to a conditional.
#
# (see EventDispatch::Base#)
#
def resource_skipped(resource, action, conditional)
return if consumers.empty?
current_record.status = :skipped
current_record.conditional = conditional
end
# Hook called after an action modifies the system and is marked updated.
#
# (see EventDispatch::Base#)
#
def resource_updated(new_resource, action)
return if consumers.empty?
current_record.status = :updated
end
# Hook called after an action fails.
#
# (see EventDispatch::Base#)
#
def resource_failed(new_resource, action, exception)
return if consumers.empty?
current_record.status = :failed
current_record.exception = exception
end
# Hook called after an action is completed. This is always called, even if the action fails.
#
# (see EventDispatch::Base#)
#
def resource_completed(new_resource)
return if consumers.empty?
current_record.elapsed_time = new_resource.elapsed_time
# Verify if the resource has sensitive data and create a new blank resource with only
# the name so we can report it back without sensitive data
# XXX?: what about sensitive data in the current_resource?
# FIXME: this needs to be display-logic
if current_record.new_resource.sensitive
klass = current_record.new_resource.class
resource_name = current_record.new_resource.name
current_record.new_resource = klass.new(resource_name)
end
action_records << pending_updates.pop
end
private
# @return [Chef::ActionCollection::ActionRecord] the current record we are working on at the top of the stack
def current_record
pending_updates[-1]
end
# If the chef-client run fails in the middle, we are left with a half-completed resource_collection, this
# method is responsible for adding all of the resources which have not yet been touched. They are marked
# as being "unprocessed".
#
def detect_unprocessed_resources
run_context.resource_collection.all_resources.select { |resource| resource.executed_by_runner == false }.each do |resource|
Array(resource.action).each do |action|
record = ActionRecord.new(resource, action, 0)
record.status = :unprocessed
action_records << record
end
end
end
end
end
|