summaryrefslogtreecommitdiff
path: root/lib/chef/action_collection.rb
blob: 1ac47630a9e1e3c172da81b0edf46a085a81403f (plain)
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
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#
# Copyright:: Copyright (c) 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

      # @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 [Chef::Resource] the after_resource object (after-state).  This can be nil for
      # non custom-resources or resources that do not implement load_after_resource.
      attr_accessor :after_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 [Hash] JSON-formatted error description from the Chef::Formatters::ErrorMapper
      attr_accessor :error_description

      # @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 current 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 after resource is loaded.  If load_after_resource fails, this hook will
    # not be called and after_resource will be nil, and the resource_failed hook will be called.
    #
    # (see EventDispatch::Base#)
    #
    def resource_after_state_loaded(new_resource, action, after_resource)
      return if consumers.empty?

      current_record.after_resource = after_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
      current_record.error_description = Formatters::ErrorMapper.resource_failed(new_resource, action, exception).for_json
    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