summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLamont Granquist <lamont@scriptkiddie.org>2019-03-11 11:49:31 -0700
committerLamont Granquist <lamont@scriptkiddie.org>2019-03-11 11:49:31 -0700
commit66015ba654469f4dacfd78d40b02aafee52bbf1b (patch)
treeb00d0de111d18980f446b006ac63ef599eea8108
parent4037976199b728d4bdc18fd428e8d40a84c97e2b (diff)
downloadchef-66015ba654469f4dacfd78d40b02aafee52bbf1b.tar.gz
Extract Action Collection from Data Collector
See the PR for details on this change. Signed-off-by: Lamont Granquist <lamont@scriptkiddie.org>
-rw-r--r--.gitignore1
-rw-r--r--chef-config/lib/chef-config/config.rb31
-rw-r--r--chef-config/spec/unit/config_spec.rb19
-rw-r--r--docs/dev/action_collection.md106
-rw-r--r--docs/dev/data_collector.md117
-rw-r--r--lib/chef/action_collection.rb252
-rw-r--r--lib/chef/client.rb43
-rw-r--r--lib/chef/data_collector.rb660
-rw-r--r--lib/chef/data_collector/config_validation.rb88
-rw-r--r--lib/chef/data_collector/error_handlers.rb116
-rw-r--r--lib/chef/data_collector/message_helpers.rb50
-rw-r--r--lib/chef/data_collector/messages.rb100
-rw-r--r--lib/chef/data_collector/messages/helpers.rb159
-rw-r--r--lib/chef/data_collector/resource_report.rb123
-rw-r--r--lib/chef/data_collector/run_end_message.rb172
-rw-r--r--lib/chef/data_collector/run_start_message.rb60
-rw-r--r--lib/chef/event_dispatch/base.rb19
-rw-r--r--lib/chef/event_dispatch/dispatcher.rb54
-rw-r--r--lib/chef/event_loggers/windows_eventlog.rb4
-rw-r--r--lib/chef/formatters/doc.rb2
-rw-r--r--lib/chef/formatters/minimal.rb2
-rw-r--r--lib/chef/node.rb24
-rw-r--r--lib/chef/policy_builder/dynamic.rb3
-rw-r--r--lib/chef/policy_builder/expand_node_object.rb17
-rw-r--r--lib/chef/policy_builder/policyfile.rb4
-rw-r--r--lib/chef/resource.rb12
-rw-r--r--lib/chef/resource_collection/resource_list.rb2
-rw-r--r--lib/chef/resource_reporter.rb183
-rw-r--r--lib/chef/run_context.rb5
-rw-r--r--lib/chef/run_context/cookbook_compiler.rb2
-rw-r--r--lib/chef/run_status.rb3
-rw-r--r--lib/chef/runner.rb8
-rw-r--r--spec/functional/event_loggers/windows_eventlog_spec.rb4
-rw-r--r--spec/spec_helper.rb2
-rw-r--r--spec/support/shared/context/client.rb305
-rw-r--r--spec/support/shared/examples/client.rb104
-rw-r--r--spec/unit/action_collection_spec.rb19
-rw-r--r--spec/unit/client_spec.rb343
-rw-r--r--spec/unit/data_collector/messages/helpers_spec.rb202
-rw-r--r--spec/unit/data_collector/messages_spec.rb329
-rw-r--r--spec/unit/data_collector/resource_report_spec.rb145
-rw-r--r--spec/unit/data_collector_spec.rb1316
-rw-r--r--spec/unit/event_dispatch/dispatcher_spec.rb55
-rw-r--r--spec/unit/node_spec.rb10
-rw-r--r--spec/unit/resource_reporter_spec.rb414
45 files changed, 2686 insertions, 3003 deletions
diff --git a/.gitignore b/.gitignore
index ac14a13e8e..200db3afc5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,6 +47,7 @@ kitchen-tests/Berksfile.lock
# Temporary files present during spec runs
spec/data/test-dir
spec/data/nodes
+spec/data/chef_guid
/config/
vendor/
diff --git a/chef-config/lib/chef-config/config.rb b/chef-config/lib/chef-config/config.rb
index 1c921eac72..29dadf54e0 100644
--- a/chef-config/lib/chef-config/config.rb
+++ b/chef-config/lib/chef-config/config.rb
@@ -378,6 +378,11 @@ module ChefConfig
default :diff_disabled, false
default :diff_filesize_threshold, 10000000
default :diff_output_threshold, 1000000
+
+ # This is true for "local mode" which uses a chef-zero server listening on
+ # localhost one way or another. This is true for both `chef-solo` (without
+ # the --legacy-mode flag) or `chef-client -z` methods of starting a client run.
+ #
default :local_mode, false
# Configures the mode of operation for ChefFS, which is applied to the
@@ -445,9 +450,29 @@ module ChefConfig
end
default :rest_timeout, 300
+
+ # This solo setting is now almost entirely useless. It is set to true if chef-solo was
+ # invoked that way from the command-line (i.e. from Application::Solo as opposed to
+ # Application::Client). The more useful information is contained in the :solo_legacy_mode
+ # vs the :local_mode flags which will be set to true or false depending on how solo was
+ # invoked and actually change more of the behavior. There might be slight differences in
+ # the behavior of :local_mode due to the behavioral differences in Application::Solo vs.
+ # Application::Client and `chef-solo` vs `chef-client -z`, but checking this value and
+ # switching based on it is almost certainly doing the wrong thing and papering over
+ # bugs that should be fixed in one or the other class, and will be brittle and destined
+ # to break in the future (and not necessarily on a major version bump). Checking this value
+ # is also not sufficent to determine if we are not running against a server since this can
+ # be unset but :local_mode may be set. It would be accurate to check both :solo and :local_mode
+ # to determine if we're not running against a server, but the more semantically accurate test
+ # is going to be combining :solo_legacy_mode and :local_mode.
+ #
+ # TL;DR: `if Chef::Config[:solo]` is almost certainly buggy code, you should use:
+ # `if Chef::Config[:local_mode] || Chef::Config[:solo_legacy_mode]`
+ #
+ # @api private
default :solo, false
- # Are we running in old Chef Solo legacy mode?
+ # This is true for old chef-solo legacy mode without any chef-zero server (chef-solo --legacy-mode)
default :solo_legacy_mode, false
default :splay, nil
@@ -919,7 +944,7 @@ module ChefConfig
# data collector will not run.
# Ex: http://my-data-collector.mycompany.com/ingest
default(:server_url) do
- if config_parent.solo || config_parent.local_mode
+ if config_parent.solo_legacy_mode || config_parent.local_mode
nil
else
File.join(config_parent.chef_server_url, "/data-collector")
@@ -950,7 +975,7 @@ module ChefConfig
# generated by the DataCollector when Chef is run in Solo mode. This
# allows users to associate their Solo nodes with faux organizations
# without the nodes being connected to an actual Chef Server.
- default :organization, nil
+ default :organization, "chef_solo"
end
configurable(:http_proxy)
diff --git a/chef-config/spec/unit/config_spec.rb b/chef-config/spec/unit/config_spec.rb
index b6e88f0bc8..0e17753185 100644
--- a/chef-config/spec/unit/config_spec.rb
+++ b/chef-config/spec/unit/config_spec.rb
@@ -1,7 +1,7 @@
#
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Kyle Goodwin (<kgoodwin@primerevenue.com>)
-# Copyright:: Copyright 2008-2016, Chef Software Inc.
+# Copyright:: Copyright 2008-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -1197,10 +1197,23 @@ RSpec.describe ChefConfig::Config do
end
- context "for Chef Solo" do
+ context "for Chef Solo legacy mode" do
before do
- ChefConfig::Config[:solo] = true
+ ChefConfig::Config[:solo_legacy_mode] = true
+ end
+
+ it "sets the data collector server URL to nil" do
+ ChefConfig::Config[:chef_server_url] = "https://chef.example/organizations/myorg"
+ expect(ChefConfig::Config[:data_collector][:server_url]).to be_nil
+ end
+
+ end
+
+ context "for local mode" do
+
+ before do
+ ChefConfig::Config[:local_mode] = true
end
it "sets the data collector server URL to nil" do
diff --git a/docs/dev/action_collection.md b/docs/dev/action_collection.md
new file mode 100644
index 0000000000..a0735f65fb
--- /dev/null
+++ b/docs/dev/action_collection.md
@@ -0,0 +1,106 @@
+---
+title: Action Collection
+---
+
+# Action Collection Design
+
+* Extract common code from the Resource Reporter and Data Collector.
+* Expose a general purpose API for querying a record of all actions taken during the Chef run.
+* Enable utilities like the 'zap' cookbook to be written to interact properly with Custom Resources.
+
+The Action Collection tracks all actions taken by all Chef resources. The resources can be in recipe code, as sub-resources of custom resources or
+they may be built "by hand". Since the Action Collection hooks the events which are fired from the `run_action` method on Chef::Resource it does
+not matter how the resources were built (as long as they were correctly passed the Chef `run_context`).
+
+This is complementary, but superior, to the resource collection which has an incomplete picture of what might happen or has happened in the run since there are
+many common ways of invoking resource actions which are not captured by how the resource collection is built. Replaying the sequence of actions in
+the Action Collection would be closer to replaying the chef-client converge than trying to re-converge the resource collection (although both of
+those models are still flawed in the presence of any imperative code that controls the shape of those objects).
+
+This design extracts common duplicated code from the Data Collection and old Resource Reporter, and is designed to be used by other consumers which
+need to ask questions like "in this run, what file resources had actions fired on them?", which can then be used to answer questions like
+"which files is Chef managing in this directory?".
+
+# Usage
+
+## Action Collection Event Hook Registration
+
+Consumers may register an event handler which hooks the `action_collection_registration` hook. This event is fired directly before recipes are
+compiled and converged (after library loading, attributes, etc). This is just before the earliest point in time that a resource should fire an
+action so represents the latest point that a consumer should make a decision about if it needs the Action Collection to be enabled or not.
+
+Consumers can hook this method. They will be passed the Action Collection instance, which can be saved by the caller to be queried later. They
+should then register themselves with the Action Collection (since without registering any interest, the Action Collection will disable itself).
+
+```ruby
+ def action_collection_registration(action_collection)
+ @action_collection = action_collection
+ action_collection.register(self)
+ end
+```
+
+## Library Registration
+
+Any cookbook library code may also register itself with the Action Collection. The Action Collection will be registered with the `run_context` after
+it is created, so registration may be accomplished easily:
+
+```ruby
+ Chef.run_context.action_collection.register(self)
+```
+
+## Action Collection Requires Registration
+
+If one of the prior methods is not used to register for the Action Collection, then the Action Collection will disable itself and will not compile
+the Action Collection in order to not waste the memory overhead of tracking the actions during the run. The Data Collector takes advantage of this
+since if the run start message from the Data Collector is refused by the server, then the Data Collector disables itself, and then does not register
+with the Action Collection, which would disable the Action Collection. This makes use of the delayed hooking through the `action_collection_regsitration`
+so that the Data Collector never registers itself after it is disabled.
+
+## Searching
+
+There is a function `filtered_collection` which returns "slices" off of the `ActionCollection` object. The `max_nesting` argument can be used to prune
+how deep into sub-resources the returned view goes (`max_nesting: 0` will return only resources in recipe context, with any hand created resources, but
+no subresources). There are also 5 different states of the action: `up_to_date`, `skipped`, `updated`, `failed`, `unprocessed` which can be filtered
+on. All of these are true by default, so they must be disabled to remove them from the filtered collection.
+
+The `ActionCollection` object itself implements enumerable and returns `ActionRecord` objects (see the `ActionCollection` code for the fields exposed on
+`ActionRecord`s).
+
+This would return all file resources in any state in the recipe context:
+
+```
+Chef.run_context.action_collection.filtered_collection(max_nesting: 0).select { |rec| rec.new_resource.is_a?(Chef::Resource::File) }
+```
+
+NOTE:
+As the Action Collection API was initially designed around the Resource Reporter and Data Collector use cases, the searching API is currently rudimentary
+and could easily lift some of the searching features on the name of the resource from the resource collection, and could use a more fluent API
+for composing searches.
+
+# Implementation Details
+
+## Resource Event Lifecycle Hooks
+
+Resources actions fire off several events in sequence:
+
+1. `resource_action_start` - this is always fired first
+2. `resource_current_state_loaded` - this is normally always second, but may be skipped in the case of a resource which throws an exception during
+`load_current_resource` (which means that the `current_resource` off the `ActionRecord` may be nil).
+3. `resource_up_to_date` / `resource_skipped` / `resource_updated` / `resource_failed` - one of these is always called which corresponds to the state of the action.
+4. `resource_completed` - this is always fired last
+
+For skipped resources, the conditional will be saved in the `ActionRecord`. For failed resources the exception is saved in the `ActionRecord`.
+
+## Unprocessed Resources
+
+The unprocessed resource concept is to report on resources which are left in the resource collection after a failure. A successful Chef run should
+never leave any unprocessed resources (`action :nothing` resources are still inspected by the resource collection and are processed). There must be
+an exception thrown during the execution of the resource collection, and the unprocessed resources were never visited by the runner that executes
+the resource collection.
+
+This list will be necessarily incomplete of any unprocessed sub-resources in custom resources, since the run was aborted before those resources
+executed actions and built their own sub-resource collections.
+
+This was a design requirement of the Data Collector.
+
+To implement this in a more sane manner the runner that evaluates the resource collection now tracks the resources that it visits.
diff --git a/docs/dev/data_collector.md b/docs/dev/data_collector.md
new file mode 100644
index 0000000000..168bf206e2
--- /dev/null
+++ b/docs/dev/data_collector.md
@@ -0,0 +1,117 @@
+---
+title: Data Collector
+---
+
+# Data Collector Design
+
+The Data Collector design and API is covered in:
+
+https://github.com/chef/chef-rfc/blob/master/rfc077-mode-agnostic-data-collection.md
+
+This document will focus entirely on the nuts and bolts of the Data Collector
+
+## Action Collection Integration
+
+Most of the work is done by a separate Action Collection to track the actions of Chef resources. If the Data Collector is not enabled, it never registers with the
+Action Collection and no work will be done by the Action Collection to track resources.
+
+## Additional Collected Information
+
+The Data Collector also collects:
+
+- the expanded run list
+- deprecations
+- the node
+- formatted error output for exceptions
+
+Most of this is done through hooking events directly in the Data Collector itself. The ErrorHandlers module is broken out into a module which is directly mixed into
+the Data Collector to separate that concern out into a different file (it is straightforward with fairly little state, but is just a lot of hooked methods).
+
+## Basic Configuration Modes
+
+### Configured for Automate
+
+Do nothing. The URL is constructed from the base `Chef::Config[:chef_server_url]`, auth is just Chef Server API authentication, and the default behavior is that it
+is configured.
+
+### Configured to Log to a File
+
+Setup a file output location, no token is necessary:
+
+```
+Chef::Config[:data_collector][:output_locations] = { files: [ "/Users/lamont/data_collector.out" ] }
+```
+
+Note the fact that you can't assign to `Chef::Config[:data_collector][:output_locations][:files]` and will NoMethodError if you try.
+
+### Configured to Log to a Non-Chef Server Endpoint
+
+Setup a server url, requiring a token:
+
+```
+Chef::Config[:data_collector][:server_url] = "https://chef.acme.local/myendpoint.html"
+Chef::Config[:data_collector][:token] = "mytoken"
+```
+
+This works for chef-clients which are configured to hit a chef server, but use a custom non-Chef-Automate endpoint for reporting, or for chef-solo/zero users.
+
+XXX: There is also the `Chef::Config[:data_collector][:output_locations] = { uri: [ "https://chef.acme.local/myendpoint.html" ] }` method -- which is going to behave
+differently, particularly on non-chef-solo use cases. In that case the Data Collector `server_url` will still be automatically derived from the `chef_server_url` and
+the Data Collector will attempt to contact that endpoint, but with the token being supplied it will use that and will not use Chef Server authentication, and the
+server should 403 back, and if `raise_on_failure` is left to the default of false then it will simply drop that failure and continue without raising, which will
+appear to work, and output will be send to the configured `output_locations`. Note that the presence of a token flips all external URIs to using the token so that
+it is **not** possible to use this feature to talk to both a Chef Automate endpoint and a custom URI reporting endpoint (which would seem to be the most useful of an
+incredibly marginally useful feature and it does not work). But given how hopelessly complicated this is, the recommendation is to use the `server_url` and to avoid
+using any `url` options in the `output_locations` since that feature is fairly poorly designed at this point in time.
+
+## Resiliency to Failures
+
+The Data Collector in Chef >= 15.0 is resilient to failures that occur anywhere in the main loop of the `Chef::Client#run` method. In order to do this there is a lot
+of defensive coding around internal data structures that may be nil (e.g. failures before the node is loaded will result in the node being nil). The spec tests for
+the Data Collector now run through a large sequence of events (which must, unfortunately, be manually kept in sync with the events in the Chef::Client if those events
+are ever 'moved' around) which should catch any issues in the Data Collector with early failures. The specs should also serve as documentation for what the messages
+will look like under different failure conditions. The goal was to keep the format of the messages to look as near as possible to the same schema as possible even
+in the presence of failures. But some data structures will be entirely empty.
+
+When the Data Collector fails extraordinarily early it still sends both a start and an end message. This will happen if it fails so early that it would not normally
+have sent a start message.
+
+## Decision to Be Enabled
+
+This is complicated due to over-design and is encapsulated in the `#should_be_enabled?` method and the ConfigValidation module. The `#should_be_enabled?` message and
+ConfigValidation should probably be merged into one renamed Config module to isolate the concern of processing the Chef::Config options and doing the correct thing.
+
+## Run Start and Run End Message modules
+
+These are separated out into their own modules, which are very deliberately not mixed into the main Data Collector. They use the Data Collector and Action Collection
+public interfaces. They are stateless themselves. This keeps the collaboration between them and the Data Collector very easy to understand. The start message is
+relatively simple and straightforwards. The complication of the end message is mostly due to walking through the Action Collection and all the collected action
+records from the entire run, along with a lot of defensive programming to deal with early errors.
+
+## Relevant Event Sequence
+
+As it happens in the actual chef-client run:
+
+1. `events.register(data_collector)`
+2. `events.register(action_collection)`
+3. `run_status.run_id = request_id`
+4. `events.run_start(Chef::VERSION, run_status)`
+ * failures during registration will cause `registration_failed(node_name, exception, config)` here and skip to #13
+ * failures during node loading will cause `node_load_failed(node_name, exception, config)` here and skip to #13
+5. `events.node_load_success(node)`
+6. `run_status.node = node`
+ * failures during run list expansion will cause `run_list_expand_failed(node, exception)` here and skip to #13
+7. `events.run_list_expanded(expansion)`
+8. `run_status.start_clock`
+9. `events.run_started(run_status)`
+ * failures during cookbook resolution will cause `events.cookbook_resolution_failed(node, exception)` here and skip to #13
+ * failures during cookbook synch will cause `events.cookbook_sync_failed(node, exception)` and skip to #13
+10. `events.cookbook_compilation_start(run_context)`
+11. < the resource events happen here which hit the Action Collection, may throw any of the other failure events >
+12. `events.converge_complete` or `events.converge_failed(exception)`
+13. `run_status.stop_clock`
+14. `run_status.exception = exception` if it failed
+15. `events.run_completed(node, run_status)` or `events.run_failed(exception, run_status)`
+
+
+
diff --git a/lib/chef/action_collection.rb b/lib/chef/action_collection.rb
new file mode 100644
index 0000000000..a74ae5c449
--- /dev/null
+++ b/lib/chef/action_collection.rb
@@ -0,0 +1,252 @@
+#
+# 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 "chef/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
diff --git a/lib/chef/client.rb b/lib/chef/client.rb
index 3347536c12..0dd6b2666f 100644
--- a/lib/chef/client.rb
+++ b/lib/chef/client.rb
@@ -3,7 +3,7 @@
# Author:: Christopher Walters (<cw@chef.io>)
# Author:: Christopher Brown (<cb@chef.io>)
# Author:: Tim Hinderliter (<tim@chef.io>)
-# Copyright:: Copyright 2008-2018, Chef Software Inc.
+# Copyright:: Copyright 2008-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -45,6 +45,7 @@ require "chef/formatters/base"
require "chef/formatters/doc"
require "chef/formatters/minimal"
require "chef/version"
+require "chef/action_collection"
require "chef/resource_reporter"
require "chef/data_collector"
require "chef/audit/audit_reporter"
@@ -249,9 +250,14 @@ class Chef
begin
runlock.save_pid
- request_id = Chef::RequestID.instance.request_id
+ events.register(Chef::DataCollector::Reporter.new(events))
+ events.register(Chef::ActionCollection.new(events))
+
+ run_status.run_id = request_id = Chef::RequestID.instance.request_id
+
run_context = nil
- events.run_start(Chef::VERSION)
+ events.run_start(Chef::VERSION, run_status)
+
logger.info("*** Chef #{Chef::VERSION} ***")
logger.info("Platform: #{RUBY_PLATFORM}")
logger.info "Chef-client pid: #{Process.pid}"
@@ -259,16 +265,12 @@ class Chef
enforce_path_sanity
run_ohai
- generate_guid
-
register unless Chef::Config[:solo_legacy_mode]
- register_data_collector_reporter
load_node
build_node
- run_status.run_id = request_id
run_status.start_clock
logger.info("Starting Chef Run for #{node.name}")
run_started
@@ -998,33 +1000,6 @@ class Chef
Chef::ReservedNames::Win32::Security.has_admin_privileges?
end
-
- # Ensure that we have a GUID for this node
- # If we've got the proper configuration, we'll simply set that.
- # If we're registed with the data collector, we'll migrate that UUID into our configuration and use that
- # Otherwise, we'll create a new GUID and save it
- def generate_guid
- Chef::Config[:chef_guid] ||=
- if File.exists?(Chef::Config[:chef_guid_path])
- File.read(Chef::Config[:chef_guid_path])
- else
- uuid = UUIDFetcher.node_uuid
- File.open(Chef::Config[:chef_guid_path], "w+") do |fh|
- fh.write(uuid)
- end
- uuid
- end
- end
-
- class UUIDFetcher
- extend Chef::DataCollector::Messages::Helpers
- end
-
- # Register the data collector reporter to send event information to the
- # data collector server
- def register_data_collector_reporter
- events.register(Chef::DataCollector::Reporter.new) if Chef::DataCollector.register_reporter?
- end
end
end
diff --git a/lib/chef/data_collector.rb b/lib/chef/data_collector.rb
index a0b4eeb4cb..f445dc2ed9 100644
--- a/lib/chef/data_collector.rb
+++ b/lib/chef/data_collector.rb
@@ -2,7 +2,7 @@
# Author:: Adam Leff (<adamleff@chef.io>)
# Author:: Ryan Cragun (<ryan@chef.io>)
#
-# Copyright:: Copyright 2012-2018, Chef Software Inc.
+# Copyright:: Copyright 2012-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,321 +18,173 @@
# limitations under the License.
#
-require "uri"
require "chef/server_api"
require "chef/http/simple_json"
require "chef/event_dispatch/base"
-require "chef/data_collector/messages"
-require "chef/data_collector/resource_report"
-require "ostruct"
require "set"
+require "chef/data_collector/run_end_message"
+require "chef/data_collector/run_start_message"
+require "chef/data_collector/config_validation"
+require "chef/data_collector/error_handlers"
class Chef
-
- # == Chef::DataCollector
- # Provides methods for determinine whether a reporter should be registered.
class DataCollector
+ # The DataCollector is mode-agnostic reporting tool which can be used with
+ # server-based and solo-based clients. It can report to a file, to an
+ # authenticated Chef Automate reporting endpoint, or to a user-supplied
+ # webhook. It sends two messages: one at the start of the run and one
+ # at the end of the run. Most early failures in the actual Chef::Client itself
+ # are reported, but parsing of the client.rb must have succeeded and some code
+ # in Chef::Application could throw so early as to prevent reporting. If
+ # exceptions are thrown both run-start and run-end messages are still sent in
+ # pairs.
+ #
+ class Reporter < EventDispatch::Base
+ include Chef::DataCollector::ErrorHandlers
- # Whether or not to enable data collection:
- # * always disabled for why run mode
- # * disabled when the user sets `Chef::Config[:data_collector][:mode]` to a
- # value that excludes the mode (client or solo) that we are running as
- # * disabled in solo mode if the user did not configure the auth token
- # * disabled if `Chef::Config[:data_collector][:server_url]` is set to a
- # falsey value
- def self.register_reporter?
- if why_run?
- Chef::Log.trace("data collector is disabled for why run mode")
- return false
- end
- unless reporter_enabled_for_current_mode?
- Chef::Log.trace("data collector is configured to only run in " \
- "#{Chef::Config[:data_collector][:mode].inspect} modes, disabling it")
- return false
- end
- unless data_collector_url_configured? || data_collector_output_locations_configured?
- Chef::Log.trace("Neither data collector URL or output locations have been configured, disabling data collector")
- return false
- end
- if solo? && !token_auth_configured?
- Chef::Log.trace("Data collector token must be configured to use Chef Automate data collector with Chef Solo")
- end
- if !solo? && token_auth_configured?
- Chef::Log.warn("Data collector token authentication is not recommended for client-server mode" \
- "Please upgrade Chef Server to 12.11.0 and remove the token from your config file " \
- "to use key based authentication instead")
- end
- true
- end
-
- def self.data_collector_url_configured?
- !!Chef::Config[:data_collector][:server_url]
- end
-
- def self.data_collector_output_locations_configured?
- !!Chef::Config[:data_collector][:output_locations]
- end
+ # @return [Chef::RunList::RunListExpansion] the expanded run list
+ attr_reader :expanded_run_list
- def self.why_run?
- !!Chef::Config[:why_run]
- end
+ # @return [Chef::RunStatus] the run status
+ attr_reader :run_status
- def self.token_auth_configured?
- !!Chef::Config[:data_collector][:token]
- end
+ # @return [Chef::Node] the chef node
+ attr_reader :node
- def self.solo?
- !!Chef::Config[:solo] || !!Chef::Config[:local_mode]
- end
+ # @return [Set<Hash>] the acculumated list of deprecation warnings
+ attr_reader :deprecations
- def self.reporter_enabled_for_current_mode?
- if Chef::Config[:solo] || Chef::Config[:local_mode]
- acceptable_modes = [:solo, :both]
- else
- acceptable_modes = [:client, :both]
- end
+ # @return [Chef::ActionCollection] the action collection object
+ attr_reader :action_collection
- acceptable_modes.include?(Chef::Config[:data_collector][:mode])
- end
+ # @return [Chef::EventDispatch::Dispatcher] the event dispatcher
+ attr_reader :events
- # == Chef::DataCollector::Reporter
- # Provides an event handler that can be registered to report on Chef
- # run data. Unlike the existing Chef::ResourceReporter event handler,
- # the DataCollector handler is not tied to a Chef Server / Chef Reporting
- # and exports its data through a webhook-like mechanism to a configured
- # endpoint.
- class Reporter < EventDispatch::Base
- attr_reader :all_resource_reports, :status, :exception, :error_descriptions,
- :expanded_run_list, :run_context, :run_status, :http,
- :current_resource_report, :enabled, :deprecations
-
- def initialize
- validate_data_collector_server_url!
- validate_data_collector_output_locations! if data_collector_output_locations
- @all_resource_reports = []
- @current_resource_loaded = nil
- @error_descriptions = {}
+ # @param events [Chef::EventDispatch::Dispatcher] the event dispatcher
+ def initialize(events)
+ @events = events
@expanded_run_list = {}
@deprecations = Set.new
- @enabled = true
-
- @http = setup_http_client(data_collector_server_url)
- if data_collector_output_locations
- @http_output_locations = setup_http_output_locations if data_collector_output_locations[:urls]
- end
- end
-
- # see EventDispatch::Base#run_started
- # Upon receipt, we will send our run start message to the
- # configured DataCollector endpoint. Depending on whether
- # the user has configured raise_on_failure, if we cannot
- # send the message, we will either disable the DataCollector
- # Reporter for the duration of this run, or we'll raise an
- # exception.
- def run_started(current_run_status)
- update_run_status(current_run_status)
-
- message = Chef::DataCollector::Messages.run_start_message(current_run_status)
- disable_reporter_on_error do
- send_to_data_collector(message)
- end
- send_to_output_locations(message) if data_collector_output_locations
- end
-
- # see EventDispatch::Base#run_completed
- # Upon receipt, we will send our run completion message to the
- # configured DataCollector endpoint.
- def run_completed(node)
- send_run_completion(status: "success")
- end
-
- # see EventDispatch::Base#run_failed
- def run_failed(exception)
- send_run_completion(status: "failure")
end
- # see EventDispatch::Base#converge_start
- # Upon receipt, we stash the run_context for use at the
- # end of the run in order to determine what resource+action
- # combinations have not yet fired so we can report on
- # unprocessed resources.
- def converge_start(run_context)
- @run_context = run_context
- end
-
- # see EventDispatch::Base#converge_complete
- # At the end of the converge, we add any unprocessed resources
- # to our report list.
- def converge_complete
- detect_unprocessed_resources
- end
-
- # see EventDispatch::Base#converge_failed
- # At the end of the converge, we add any unprocessed resources
- # to our report list
- def converge_failed(exception)
- detect_unprocessed_resources
- end
-
- # see EventDispatch::Base#resource_current_state_loaded
- # Create a new ResourceReport instance that we'll use to track
- # the state of this resource during the run. Nested resources are
- # ignored as they are assumed to be an inline resource of a custom
- # resource, and we only care about tracking top-level resources.
- def resource_current_state_loaded(new_resource, action, current_resource)
- return if nested_resource?(new_resource)
- initialize_resource_report_if_needed(new_resource, action, current_resource)
- end
-
- # see EventDispatch::Base#resource_up_to_date
- # Mark our ResourceReport status accordingly
- def resource_up_to_date(new_resource, action)
- initialize_resource_report_if_needed(new_resource, action)
- current_resource_report.up_to_date unless nested_resource?(new_resource)
- end
-
- # see EventDispatch::Base#resource_skipped
- # If this is a top-level resource, we create a ResourceReport
- # instance (because a skipped resource does not trigger the
- # resource_current_state_loaded event), and flag it as skipped.
- def resource_skipped(new_resource, action, conditional)
- return if nested_resource?(new_resource)
-
- initialize_resource_report_if_needed(new_resource, action)
- current_resource_report.skipped(conditional)
- end
-
- # see EventDispatch::Base#resource_updated
- # Flag the current ResourceReport instance as updated (as long as it's
- # a top-level resource).
- def resource_updated(new_resource, action)
- initialize_resource_report_if_needed(new_resource, action)
- current_resource_report.updated unless nested_resource?(new_resource)
- end
-
- # see EventDispatch::Base#resource_failed
- # Flag the current ResourceReport as failed and supply the exception as
- # long as it's a top-level resource, and update the run error text
- # with the proper Formatter.
- def resource_failed(new_resource, action, exception)
- initialize_resource_report_if_needed(new_resource, action)
- current_resource_report.failed(exception) unless nested_resource?(new_resource)
- update_error_description(
- Formatters::ErrorMapper.resource_failed(
- new_resource,
- action,
- exception
- ).for_json
- )
+ # Hook to grab the run_status. We also make the decision to run or not run here (our
+ # config has been parsed so we should know if we need to run, we unregister if we do
+ # not want to run).
+ #
+ # (see EventDispatch::Base#run_start)
+ #
+ def run_start(chef_version, run_status)
+ events.unregister(self) unless should_be_enabled?
+ @run_status = run_status
end
- # see EventDispatch::Base#resource_completed
- # Mark the ResourceReport instance as finished (for timing details).
- # This marks the end of this resource during this run.
- def resource_completed(new_resource)
- if current_resource_report && !nested_resource?(new_resource)
- current_resource_report.finish
- add_resource_report(current_resource_report)
- clear_current_resource_report
- end
+ # Hook to grab the node object after it has been successfully loaded
+ #
+ # (see EventDispatch::Base#node_load_success)
+ #
+ def node_load_success(node)
+ @node = node
end
- # see EventDispatch::Base#run_list_expanded
# The expanded run list is stored for later use by the run_completed
# event and message.
+ #
+ # (see EventDispatch::Base#run_list_expanded)
+ #
def run_list_expanded(run_list_expansion)
@expanded_run_list = run_list_expansion
end
- # see EventDispatch::Base#run_list_expand_failed
- # The run error text is updated with the output of the appropriate
- # formatter.
- def run_list_expand_failed(node, exception)
- update_error_description(
- Formatters::ErrorMapper.run_list_expand_failed(
- node,
- exception
- ).for_json
- )
+ # Hook event to register with the action_collection if we are still enabled.
+ #
+ # This is also how we wire up to the action_collection since it passes itself as the argument.
+ #
+ # (see EventDispatch::Base#action_collection_registration)
+ #
+ def action_collection_registration(action_collection)
+ @action_collection = action_collection
+ action_collection.register(self)
end
- # see EventDispatch::Base#cookbook_resolution_failed
- # The run error text is updated with the output of the appropriate
- # formatter.
- def cookbook_resolution_failed(expanded_run_list, exception)
- update_error_description(
- Formatters::ErrorMapper.cookbook_resolution_failed(
- expanded_run_list,
- exception
- ).for_json
- )
- end
+ # - Creates and writes our NodeUUID back to the node object
+ # - Sanity checks the data collector
+ # - Sends the run start message
+ # - If the run_start message fails, this may disable the rest of data collection or fail hard
+ #
+ # (see EventDispatch::Base#run_started)
+ #
+ def run_started(run_status)
+ Chef::DataCollector::ConfigValidation.validate_server_url!
+ Chef::DataCollector::ConfigValidation.validate_output_locations!
- # see EventDispatch::Base#cookbook_sync_failed
- # The run error text is updated with the output of the appropriate
- # formatter.
- def cookbook_sync_failed(cookbooks, exception)
- update_error_description(
- Formatters::ErrorMapper.cookbook_sync_failed(
- cookbooks,
- exception
- ).for_json
- )
+ send_run_start
end
- # see EventDispatch::Base#deprecation
- # Append a received deprecation to the list of deprecations
+ # Hook event to accumulating deprecation messages
+ #
+ # (see EventDispatch::Base#deprecation)
+ #
def deprecation(message, location = caller(2..2)[0])
- add_deprecation(message.message, message.url, location)
+ @deprecations << { message: message.message, url: message.url, location: message.location }
+ end
+
+ # Hook to send the run completion message with a status of success
+ #
+ # (see EventDispatch::Base#run_completed)
+ #
+ def run_completed(node)
+ send_run_completion("success")
+ end
+
+ # Hook to send the run completion message with a status of failed
+ #
+ # (see EventDispatch::Base#run_failed)
+ #
+ def run_failed(exception)
+ send_run_completion("failure")
end
private
- # Selects the type of HTTP client to use based on whether we are using
- # token-based or signed header authentication. Token authentication is
- # intended to be used primarily for Chef Solo in which case no signing
- # key will be available (in which case `Chef::ServerAPI.new()` would
- # raise an exception.
+ # Construct a http client for either the main data collector or for the http output_locations.
+ #
+ # Note that based on the token setting either the main data collector and all the http output_locations
+ # are going to all require chef-server authentication or not. There is no facility to mix-and-match on
+ # a per-url basis.
+ #
+ # @param url [String] the string url to connect to
+ # @returns [Chef::HTTP] the appropriate Chef::HTTP subclass instance to use
+ #
def setup_http_client(url)
- if data_collector_token.nil?
+ if Chef::Config[:data_collector][:token].nil?
Chef::ServerAPI.new(url, validate_utf8: false)
else
Chef::HTTP::SimpleJSON.new(url, validate_utf8: false)
end
end
- def setup_http_output_locations
- Chef::Config[:data_collector][:output_locations][:urls].each_with_object({}) do |location_url, http_output_locations|
- http_output_locations[location_url] = setup_http_client(location_url)
- end
- end
-
+ # Handle POST'ing data to the data collector. Note that this is a totally separate concern
+ # from the array of URI's in the extra configured output_locations.
#
- # Yields to the passed-in block (which is expected to be some interaction
- # with the DataCollector endpoint). If some communication failure occurs,
- # either disable any future communications to the DataCollector endpoint, or
- # raise an exception (if the user has set
- # Chef::Config.data_collector.raise_on_failure to true.)
+ # On failure this will unregister the data collector (if there are no other configured output_locations)
+ # and optionally will either silently continue or fail hard depending on configuration.
#
- # @param block [Proc] A ruby block to run. Ignored if a command is given.
+ # @param message [Hash] message to send
#
- def disable_reporter_on_error
- yield
+ def send_to_data_collector(message)
+ return unless Chef::Config[:data_collector][:server_url]
+ @http ||= setup_http_client(Chef::Config[:data_collector][:server_url])
+ @http.post(nil, message, headers)
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET,
- Errno::ECONNREFUSED, EOFError, Net::HTTPBadResponse,
- Net::HTTPHeaderSyntaxError, Net::ProtocolError, OpenSSL::SSL::SSLError,
- Errno::EHOSTDOWN => e
+ Errno::ECONNREFUSED, EOFError, Net::HTTPBadResponse,
+ Net::HTTPHeaderSyntaxError, Net::ProtocolError, OpenSSL::SSL::SSLError,
+ Errno::EHOSTDOWN => e
# Do not disable data collector reporter if additional output_locations have been specified
- disable_data_collector_reporter unless data_collector_output_locations
- code = if e.respond_to?(:response) && e.response.code
- e.response.code.to_s
- else
- "Exception Code Empty"
- end
+ events.unregister(self) unless Chef::Config[:data_collector][:output_locations]
+
+ code = e&.response&.code&.to_s || "Exception Code Empty"
- msg = "Error while reporting run start to Data Collector. " \
- "URL: #{data_collector_server_url} " \
- "Exception: #{code} -- #{e.message} "
+ msg = "Error while reporting run start to Data Collector. URL: #{Chef::Config[:data_collector][:server_url]} Exception: #{code} -- #{e.message} "
if Chef::Config[:data_collector][:raise_on_failure]
Chef::Log.error(msg)
@@ -344,222 +196,128 @@ class Chef
end
end
- def send_to_data_collector(message)
- return unless data_collector_accessible?
- http.post(nil, message, headers) if data_collector_server_url
- end
-
+ # Process sending the configured message to all the extra output locations.
+ #
+ # @param message [Hash] message to send
+ #
def send_to_output_locations(message)
- data_collector_output_locations.each do |type, location_list|
- location_list.each do |l|
- handle_output_location(type, l, message)
+ return unless Chef::Config[:data_collector][:output_locations]
+
+ Chef::Config[:data_collector][:output_locations].each do |type, locations|
+ locations.each do |location|
+ send_to_file_location(location, message) if type == :files
+ send_to_http_location(location, message) if type == :urls
end
end
end
- def handle_output_location(type, loc, message)
- type == :urls ? send_to_http_location(loc, message) : send_to_file_location(loc, message)
- end
-
+ # Sends a single message to a file, rendered as JSON.
+ #
+ # @param file_name [String] the file to write to
+ # @param message [Hash] the message to render as JSON
+ #
def send_to_file_location(file_name, message)
- open(file_name, "a") { |f| f.puts message }
+ File.open(file_name, "a") do |fh|
+ fh.puts Chef::JSONCompat.to_json(message)
+ end
end
+ # Sends a single message to a http uri, rendered as JSON. Maintains a cache of Chef::HTTP
+ # objects to use on subsequent requests.
+ #
+ # @param http_url [String] the configured http uri string endpoint to send to
+ # @param message [Hash] the message to render as JSON
+ #
def send_to_http_location(http_url, message)
- @http_output_locations[http_url].post(nil, message, headers) if @http_output_locations[http_url]
+ @http_output_locations_clients[http_url] ||= setup_http_client(http_url)
+ @http_output_locations_clients[http_url].post(nil, message, headers)
rescue
- Chef::Log.trace("Data collector failed to send to URL location #{http_url}. Please check your configured data_collector.output_locations")
+ # FIXME: we do all kinds of complexity to deal with errors in send_to_data_collector and we just don't care here, which feels like
+ # like poor behavior on several different levels, at least its a warn now... (I don't quite understand why it was written this way)
+ Chef::Log.warn("Data collector failed to send to URL location #{http_url}. Please check your configured data_collector.output_locations")
end
+ # @return [Boolean] if we've sent a run_start message yet
+ def sent_run_start?
+ !!@sent_run_start
+ end
+
+ # Send the run start message to the configured server or output locations
#
- # Send any messages to the DataCollector endpoint that are necessary to
- # indicate the run has completed. Currently, two messages are sent:
- #
- # - An "action" message with the node object indicating it's been updated
- # - An "run_converge" (i.e. RunEnd) message with details about the run,
- # what resources were modified/up-to-date/skipped, etc.
+ def send_run_start
+ message = Chef::DataCollector::RunStartMessage.construct_message(self)
+ send_to_data_collector(message)
+ send_to_output_locations(message)
+ @sent_run_start = true
+ end
+
+ # Send the run completion message to the configured server or output locations
#
- # @param opts [Hash] Additional details about the run, such as its success/failure.
+ # @param status [String] Either "success" or "failed"
#
- def send_run_completion(opts)
- # If run_status is nil we probably failed before the client triggered
- # the run_started callback. In this case we'll skip updating because
- # we have nothing to report.
- return unless run_status
+ def send_run_completion(status)
+ # this is necessary to send a run_start message when we fail before the run_started chef event.
+ # we adhere to a contract that run_start + run_completion events happen in pairs.
+ send_run_start unless sent_run_start?
- message = Chef::DataCollector::Messages.run_end_message(
- run_status: run_status,
- expanded_run_list: expanded_run_list,
- resources: all_resource_reports,
- status: opts[:status],
- error_descriptions: error_descriptions,
- deprecations: deprecations.to_a
- )
- disable_reporter_on_error do
- send_to_data_collector(message)
- end
- send_to_output_locations(message) if data_collector_output_locations
+ message = Chef::DataCollector::RunEndMessage.construct_message(self, status)
+ send_to_data_collector(message)
+ send_to_output_locations(message)
end
+ # @return [Hash] HTTP headers for the data collector endpoint
def headers
headers = { "Content-Type" => "application/json" }
- unless data_collector_token.nil?
- headers["x-data-collector-token"] = data_collector_token
+ unless Chef::Config[:data_collector][:token].nil?
+ headers["x-data-collector-token"] = Chef::Config[:data_collector][:token]
headers["x-data-collector-auth"] = "version=1.0"
end
headers
end
- def data_collector_server_url
- Chef::Config[:data_collector][:server_url]
- end
-
- def data_collector_output_locations
- Chef::Config[:data_collector][:output_locations]
- end
-
- def data_collector_token
- Chef::Config[:data_collector][:token]
- end
-
- def add_resource_report(resource_report)
- @all_resource_reports << OpenStruct.new(
- resource: resource_report.new_resource,
- action: resource_report.action,
- report_data: resource_report.to_h
- )
- end
-
- def disable_data_collector_reporter
- @enabled = false
- end
-
- def data_collector_accessible?
- @enabled
- end
-
- def update_run_status(run_status)
- @run_status = run_status
- end
-
- def update_error_description(discription_hash)
- @error_descriptions = discription_hash
- end
-
- def add_deprecation(message, url, location)
- @deprecations << { message: message, url: url, location: location }
- end
-
- def initialize_resource_report_if_needed(new_resource, action, current_resource = nil)
- return unless current_resource_report.nil?
- @current_resource_report = create_resource_report(new_resource, action, current_resource)
- end
-
- def create_resource_report(new_resource, action, current_resource = nil)
- Chef::DataCollector::ResourceReport.new(
- new_resource,
- action,
- current_resource
- )
- end
-
- def clear_current_resource_report
- @current_resource_report = nil
- end
-
- def detect_unprocessed_resources
- # create a Hash (for performance reasons, rather than an Array) containing all
- # resource+action combinations from the Resource Collection
- #
- # We use the object ID instead of the resource itself in the Hash key because
- # we currently allow users to create a property called "hash" which creates
- # a #hash instance method on the resource. Ruby expects that to be a Fixnum,
- # so bad things happen when adding an object to an Array or a Hash if it's not.
- collection_resources = {}
- run_context.resource_collection.all_resources.each do |resource|
- Array(resource.action).each do |action|
- collection_resources[[resource.__id__, action]] = resource
- end
- end
-
- # Delete from the Hash any resource+action combination we have
- # already processed.
- all_resource_reports.each do |report|
- collection_resources.delete([report.resource.__id__, report.action])
- end
-
- # The items remaining in the Hash are unprocessed resource+actions,
- # so we'll create new resource reports for them which default to
- # a state of "unprocessed".
- collection_resources.each do |key, resource|
- # The Hash key is an array of the Resource's object ID and the action.
- # We need to pluck out the action.
- add_resource_report(create_resource_report(resource, key[1]))
- end
- end
-
- # If we are getting messages about a resource while we are in the middle of
- # another resource's update, we assume that the nested resource is just the
- # implementation of a provider, and we want to hide it from the reporting
- # output.
- def nested_resource?(new_resource)
- @current_resource_report && @current_resource_report.new_resource != new_resource
- end
-
- def validate_and_return_uri(uri)
- URI(uri)
- rescue URI::InvalidURIError
- nil
- end
-
- def validate_and_create_file(file)
- send_to_file_location(file, "")
- true
- # Rescue exceptions raised by the file path being non-existent or not writeable and re-raise them to the user
- # with clearer explanatory text.
- rescue Errno::ENOENT
- raise Chef::Exceptions::ConfigurationError,
- "Chef::Config[:data_collector][:output_locations][:files] contains the location #{file}, which is a non existent file path."
- rescue Errno::EACCES
- raise Chef::Exceptions::ConfigurationError,
- "Chef::Config[:data_collector][:output_locations][:files] contains the location #{file}, which cannnot be written to by Chef."
- end
-
- def validate_data_collector_server_url!
- unless !data_collector_server_url && data_collector_output_locations
- uri = validate_and_return_uri(data_collector_server_url)
- unless uri
- raise Chef::Exceptions::ConfigurationError, "Chef::Config[:data_collector][:server_url] (#{data_collector_server_url}) is not a valid URI."
- end
-
- if uri.host.nil?
- raise Chef::Exceptions::ConfigurationError,
- "Chef::Config[:data_collector][:server_url] (#{data_collector_server_url}) is a URI with no host. Please supply a valid URL."
- end
+ # Main logic controlling the data collector being enabled or disabled:
+ #
+ # * disabled in why-run mode
+ # * disabled when `Chef::Config[:data_collector][:mode]` excludes the solo-vs-client mode
+ # * disabled if there is no server_url or no output_locations to log to
+ # * enabled if there is a configured output_location even without a token
+ # * disabled in solo mode if the user did not configure the auth token
+ #
+ # @return [Boolean] true if the data collector should be enabled
+ #
+ def should_be_enabled?
+ running_mode = ( Chef::Config[:solo_legacy_mode] || Chef::Config[:local_mode] ) ? :solo : :client
+ want_mode = Chef::Config[:data_collector][:mode]
+
+ case
+ when Chef::Config[:why_run]
+ Chef::Log.trace("data collector is disabled for why run mode")
+ return false
+ when (want_mode != :both) && running_mode != want_mode
+ Chef::Log.trace("data collector is configured to only run in #{Chef::Config[:data_collector][:mode]} modes, disabling it")
+ return false
+ when !(Chef::Config[:data_collector][:server_url] || Chef::Config[:data_collector][:output_locations])
+ Chef::Log.trace("Neither data collector URL or output locations have been configured, disabling data collector")
+ return false
+ when running_mode == :client && Chef::Config[:data_collector][:token]
+ Chef::Log.warn("Data collector token authentication is not recommended for client-server mode. " \
+ "Please upgrade Chef Server to 12.11.0 and remove the token from your config file " \
+ "to use key based authentication instead")
+ return true
+ when Chef::Config[:data_collector][:output_locations] && Chef::Config[:data_collector][:output_locations][:files] && !Chef::Config[:data_collector][:output_locations][:files].empty?
+ # we can run fine to a file without a token, even in solo mode.
+ return true
+ when running_mode == :solo && !Chef::Config[:data_collector][:token]
+ # we are in solo mode and are not logging to a file, so must have a token
+ Chef::Log.trace("Data collector token must be configured to use Chef Automate data collector with Chef Solo")
+ return false
+ else
+ return true
end
end
- def handle_type(type, loc)
- type == :urls ? validate_and_return_uri(loc) : validate_and_create_file(loc)
- end
-
- def validate_data_collector_output_locations!
- if data_collector_output_locations.empty?
- raise Chef::Exceptions::ConfigurationError,
- "Chef::Config[:data_collector][:output_locations] is empty. Please supply an hash of valid URLs and / or local file paths."
- end
-
- data_collector_output_locations.each do |type, locations|
- locations.each do |l|
- unless handle_type(type, l)
- raise Chef::Exceptions::ConfigurationError,
- "Chef::Config[:data_collector][:output_locations] contains the location #{l} which is not valid."
- end
- end
- end
- end
end
end
end
diff --git a/lib/chef/data_collector/config_validation.rb b/lib/chef/data_collector/config_validation.rb
new file mode 100644
index 0000000000..670b59dd80
--- /dev/null
+++ b/lib/chef/data_collector/config_validation.rb
@@ -0,0 +1,88 @@
+#
+# Copyright:: Copyright 2012-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 "uri"
+
+class Chef
+ class DataCollector
+ module ConfigValidation
+ class << self
+ def validate_server_url!
+ # if we have a server_url set we ALWAYS validate it, and we MUST have an output_location set to skip server_url validation
+ # (having output_locations set and no server_url is valid, but both of them unset blows up in here)
+ return if !Chef::Config[:data_collector][:server_url] && Chef::Config[:data_collector][:output_locations]
+
+ begin
+ uri = URI(Chef::Config[:data_collector][:server_url])
+ rescue
+ raise Chef::Exceptions::ConfigurationError, "Chef::Config[:data_collector][:server_url] (#{Chef::Config[:data_collector][:server_url]}) is not a valid URI."
+ end
+
+ if uri.host.nil?
+ raise Chef::Exceptions::ConfigurationError,
+ "Chef::Config[:data_collector][:server_url] (#{Chef::Config[:data_collector][:server_url]}) is a URI with no host. Please supply a valid URL."
+ end
+ end
+
+ def validate_output_locations!
+ # not having an output_location set at all is fine, we just skip it then
+ output_locations = Chef::Config[:data_collector][:output_locations]
+ return unless output_locations
+
+ # but deliberately setting an empty output_location we consider to be an error (XXX: but should we?)
+ if output_locations.empty?
+ raise Chef::Exceptions::ConfigurationError,
+ "Chef::Config[:data_collector][:output_locations] is empty. Please supply an hash of valid URLs and / or local file paths."
+ end
+
+ # loop through all the types and locations and validate each one-by-one
+ output_locations.each do |type, locations|
+ locations.each do |location|
+ validate_url!(location) if type == :urls
+ validate_file!(location) if type == :files
+ end
+ end
+ end
+
+ private
+
+ # validate an output_location file
+ def validate_file!(file)
+ open(file, "a") {}
+ rescue Errno::ENOENT
+ raise Chef::Exceptions::ConfigurationError,
+ "Chef::Config[:data_collector][:output_locations][:files] contains the location #{file}, which is a non existent file path."
+ rescue Errno::EACCES
+ raise Chef::Exceptions::ConfigurationError,
+ "Chef::Config[:data_collector][:output_locations][:files] contains the location #{file}, which cannnot be written to by Chef."
+ rescue Exception => e
+ raise Chef::Exceptions::ConfigurationError,
+ "Chef::Config[:data_collector][:output_locations][:files] contains the location #{file}, which is invalid: #{e.message}."
+ end
+
+ # validate an output_location url
+ def validate_url!(url)
+ URI(url)
+ rescue
+ raise Chef::Exceptions::ConfigurationError,
+ "Chef::Config[:data_collector][:output_locations][:urls] contains the url #{url} which is not valid."
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/data_collector/error_handlers.rb b/lib/chef/data_collector/error_handlers.rb
new file mode 100644
index 0000000000..bb892c457c
--- /dev/null
+++ b/lib/chef/data_collector/error_handlers.rb
@@ -0,0 +1,116 @@
+#
+# Copyright:: Copyright 2012-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.
+#
+
+class Chef
+ class DataCollector
+
+ # This module isolates the handling of collecting error descriptions to insert into the data_colletor
+ # report output. For very early errors it is repsonsible for collecting the node_name for the report
+ # to use. For all failure conditions that have an ErrorMapper it collects the output.
+ #
+ # No external code should call anything in this module directly.
+ #
+ # @api private
+ #
+ module ErrorHandlers
+
+ # @return [String] the fallback node name if we do NOT have a node due to early failures
+ attr_reader :node_name
+
+ # @return [Hash] JSON-formatted error description from the Chef::Formatters::ErrorMapper
+ def error_description
+ @error_description ||= {}
+ end
+
+ # This is an exceptionally "early" failure that results in not having a valid Chef::Node object,
+ # so it must capture the node_name from the config.rb
+ #
+ # (see EventDispatch::Base#registration_failed)
+ #
+ def registration_failed(node_name, exception, config)
+ description = Formatters::ErrorMapper.registration_failed(node_name, exception, config)
+ @node_name = node_name
+ @error_description = description.for_json
+ end
+
+ # This is an exceptionally "early" failure that results in not having a valid Chef::Node object,
+ # so it must capture the node_name from the config.rb
+ #
+ # (see EventDispatch::Base#node_load_failed)
+ #
+ def node_load_failed(node_name, exception, config)
+ description = Formatters::ErrorMapper.node_load_failed(node_name, exception, config)
+ @node_name = node_name
+ @error_description = description.for_json
+ end
+
+ # This is an "early" failure during run_list expansion
+ #
+ # (see EventDispatch::Base#run_list_expand_failed)
+ #
+ def run_list_expand_failed(node, exception)
+ description = Formatters::ErrorMapper.run_list_expand_failed(node, exception)
+ @error_description = description.for_json
+ end
+
+ # This is an "early" failure during cookbook resolution / depsolving / talking to cookbook_version endpoint on a server
+ #
+ # (see EventDispatch::Base#cookbook_resolution_failed)
+ #
+ def cookbook_resolution_failed(expanded_run_list, exception)
+ description = Formatters::ErrorMapper.cookbook_resolution_failed(expanded_run_list, exception)
+ @error_description = description.for_json
+ end
+
+ # This is an "early" failure during cookbook synchronization
+ #
+ # (see EventDispatch::Base#cookbook_sync_failed)
+ #
+ def cookbook_sync_failed(cookbooks, exception)
+ description = Formatters::ErrorMapper.cookbook_sync_failed(cookbooks, exception)
+ @error_description = description.for_json
+ end
+
+ # This failure happens during library loading / attribute file parsing, etc.
+ #
+ # (see EventDispatch::Base#file_load_failed)
+ #
+ def file_load_failed(path, exception)
+ description = Formatters::ErrorMapper.file_load_failed(path, exception)
+ @error_description = description.for_json
+ end
+
+ # This failure happens at converge time during recipe parsing
+ #
+ # (see EventDispatch::Base#recipe_not_failed)
+ #
+ def recipe_not_found(exception)
+ description = Formatters::ErrorMapper.file_load_failed(nil, exception)
+ @error_description = description.for_json
+ end
+
+ # This is a normal resource failure event during compile/converge phases
+ #
+ # (see EventDispatch::Base#resource_failed)
+ #
+ def resource_failed(new_resource, action, exception)
+ description = Formatters::ErrorMapper.resource_failed(new_resource, action, exception)
+ @error_description = description.for_json
+ end
+ end
+ end
+end
diff --git a/lib/chef/data_collector/message_helpers.rb b/lib/chef/data_collector/message_helpers.rb
new file mode 100644
index 0000000000..75783d8fd0
--- /dev/null
+++ b/lib/chef/data_collector/message_helpers.rb
@@ -0,0 +1,50 @@
+#
+# Copyright:: Copyright 2012-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.
+#
+
+class Chef
+ class DataCollector
+
+ # This is for shared code between the run_start_message and run_end_message modules.
+ #
+ # No external code should call this module directly
+ #
+ # @api private
+ #
+ module MessageHelpers
+ private
+
+ # The organization name the node is associated with. For Chef Solo runs the default
+ # is "chef_solo" which can be overridden by the user.
+ #
+ # @return [String] Chef organization associated with the node
+ #
+ def organization
+ if solo_run?
+ # configurable fake organization name for chef-solo users
+ Chef::Config[:data_collector][:organization]
+ else
+ Chef::Config[:chef_server_url].match(%r{/+organizations/+([^\s/]+)}).nil? ? "unknown_organization" : $1
+ end
+ end
+
+ # @return [Boolean] True if we're in a chef-solo/chef-zero or legacy chef-solo run
+ def solo_run?
+ Chef::Config[:solo_legacy_mode] || Chef::Config[:local_mode]
+ end
+ end
+ end
+end
diff --git a/lib/chef/data_collector/messages.rb b/lib/chef/data_collector/messages.rb
deleted file mode 100644
index c375475c72..0000000000
--- a/lib/chef/data_collector/messages.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-#
-# Author:: Adam Leff (<adamleff@chef.io)
-# Author:: Ryan Cragun (<ryan@chef.io>)
-#
-# Copyright:: Copyright 2012-2016, 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 "securerandom"
-require_relative "messages/helpers"
-
-class Chef
- class DataCollector
- module Messages
- extend Helpers
-
- #
- # Message payload that is sent to the DataCollector server at the
- # start of a Chef run.
- #
- # @param run_status [Chef::RunStatus] The RunStatus instance for this node/run.
- #
- # @return [Hash] A hash containing the run start message data.
- #
- def self.run_start_message(run_status)
- {
- "chef_server_fqdn" => chef_server_fqdn,
- "entity_uuid" => node_uuid,
- "id" => run_status.run_id,
- "message_version" => "1.0.0",
- "message_type" => "run_start",
- "node_name" => run_status.node.name,
- "organization_name" => organization,
- "run_id" => run_status.run_id,
- "source" => collector_source,
- "start_time" => run_status.start_time.utc.iso8601,
- }
- end
-
- #
- # Message payload that is sent to the DataCollector server at the
- # end of a Chef run.
- #
- # @param reporter_data [Hash] Data supplied by the Reporter, such as run_status, resource counts, etc.
- #
- # @return [Hash] A hash containing the run end message data.
- #
- def self.run_end_message(reporter_data)
- run_status = reporter_data[:run_status]
-
- message = {
- "chef_server_fqdn" => chef_server_fqdn,
- "entity_uuid" => node_uuid,
- "expanded_run_list" => reporter_data[:expanded_run_list],
- "id" => run_status.run_id,
- "message_version" => "1.1.0",
- "message_type" => "run_converge",
- "node" => run_status.node,
- "node_name" => run_status.node.name,
- "organization_name" => organization,
- "resources" => reporter_data[:resources].map(&:report_data),
- "run_id" => run_status.run_id,
- "run_list" => run_status.node.run_list.for_json,
- "policy_name" => run_status.node.policy_name,
- "policy_group" => run_status.node.policy_group,
- "start_time" => run_status.start_time.utc.iso8601,
- "end_time" => run_status.end_time.utc.iso8601,
- "source" => collector_source,
- "status" => reporter_data[:status],
- "total_resource_count" => reporter_data[:resources].count,
- "updated_resource_count" => reporter_data[:resources].select { |r| r.report_data["status"] == "updated" }.count,
- "deprecations" => reporter_data[:deprecations],
- }
-
- if run_status.exception
- message["error"] = {
- "class" => run_status.exception.class,
- "message" => run_status.exception.message,
- "backtrace" => run_status.exception.backtrace,
- "description" => reporter_data[:error_descriptions],
- }
- end
-
- message
- end
- end
- end
-end
diff --git a/lib/chef/data_collector/messages/helpers.rb b/lib/chef/data_collector/messages/helpers.rb
deleted file mode 100644
index e4eda5ebb2..0000000000
--- a/lib/chef/data_collector/messages/helpers.rb
+++ /dev/null
@@ -1,159 +0,0 @@
-#
-# Author:: Adam Leff (<adamleff@chef.io)
-# Author:: Ryan Cragun (<ryan@chef.io>)
-#
-# Copyright:: Copyright 2012-2016, 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.
-#
-
-class Chef
- class DataCollector
- module Messages
- module Helpers
- #
- # Fully-qualified domain name of the Chef Server configured in Chef::Config
- # If the chef_server_url cannot be parsed as a URI, the node["fqdn"] attribute
- # will be returned, or "localhost" if the run_status is unavailable to us.
- #
- # @return [String] FQDN of the configured Chef Server, or node/localhost if not found.
- #
- def chef_server_fqdn
- if !Chef::Config[:chef_server_url].nil?
- URI(Chef::Config[:chef_server_url]).host
- elsif !Chef::Config[:node_name].nil?
- Chef::Config[:node_name]
- else
- "localhost"
- end
- end
-
- #
- # The organization name the node is associated with. For Chef Solo runs, a
- # user-configured organization string is returned, or the string "chef_solo"
- # if such a string is not configured.
- #
- # @return [String] Organization to which the node is associated
- #
- def organization
- solo_run? ? data_collector_organization : chef_server_organization
- end
-
- #
- # Returns the user-configured organization, or "chef_solo" if none is configured.
- #
- # This is only used when Chef is run in Solo mode.
- #
- # @return [String] Data-collector-specific organization used when running in Chef Solo
- #
- def data_collector_organization
- Chef::Config[:data_collector][:organization] || "chef_solo"
- end
-
- #
- # Return the organization assumed by the configured chef_server_url.
- #
- # We must parse this from the Chef::Config[:chef_server_url] because a node
- # has no knowledge of an organization or to which organization is belongs.
- #
- # If we cannot determine the organization, we return "unknown_organization"
- #
- # @return [String] shortname of the Chef Server organization
- #
- def chef_server_organization
- return "unknown_organization" unless Chef::Config[:chef_server_url]
-
- Chef::Config[:chef_server_url].match(%r{/+organizations/+([a-z0-9][a-z0-9_-]{0,254})}).nil? ? "unknown_organization" : $1
- end
-
- #
- # The source of the data collecting during this run, used by the
- # DataCollector endpoint to determine if Chef was in Solo mode or not.
- #
- # @return [String] "chef_solo" if in Solo mode, "chef_client" if in Client mode
- #
- def collector_source
- solo_run? ? "chef_solo" : "chef_client"
- end
-
- #
- # If we're running in Solo (legacy) mode, or in Solo (formerly
- # "Chef Client Local Mode"), we're considered to be in a "solo run".
- #
- # @return [Boolean] Whether we're in a solo run or not
- #
- def solo_run?
- Chef::Config[:solo] || Chef::Config[:local_mode]
- end
-
- #
- # Returns a UUID that uniquely identifies this node for reporting reasons.
- #
- # The node is read in from disk if it exists, or it's generated if it does
- # does not exist.
- #
- # @return [String] UUID for the node
- #
- def node_uuid
- Chef::Config[:chef_guid] || read_node_uuid || generate_node_uuid
- end
-
- #
- # Generates a UUID for the node via SecureRandom.uuid and writes out
- # metadata file so the UUID persists between runs.
- #
- # @return [String] UUID for the node
- #
- def generate_node_uuid
- uuid = SecureRandom.uuid
- update_metadata("node_uuid", uuid)
-
- uuid
- end
-
- #
- # Reads in the node UUID from the node metadata file
- #
- # @return [String] UUID for the node
- #
- def read_node_uuid
- metadata["node_uuid"]
- end
-
- #
- # Returns the DataCollector metadata for this node
- #
- # If the metadata file does not exist in the file cache path,
- # an empty hash will be returned.
- #
- # @return [Hash] DataCollector metadata for this node
- #
- def metadata
- Chef::JSONCompat.parse(Chef::FileCache.load(metadata_filename))
- rescue Chef::Exceptions::FileNotFound
- {}
- end
-
- def update_metadata(key, value)
- updated_metadata = metadata.tap { |x| x[key] = value }
- Chef::FileCache.store(metadata_filename, Chef::JSONCompat.to_json(updated_metadata), 0644)
- end
-
- def metadata_filename
- "data_collector_metadata.json"
- end
- end
- end
- end
-end
diff --git a/lib/chef/data_collector/resource_report.rb b/lib/chef/data_collector/resource_report.rb
deleted file mode 100644
index 9a99747f7f..0000000000
--- a/lib/chef/data_collector/resource_report.rb
+++ /dev/null
@@ -1,123 +0,0 @@
-#
-# Author:: Adam Leff (<adamleff@chef.io>)
-# Author:: Ryan Cragun (<ryan@chef.io>)
-#
-# Copyright:: Copyright 2012-2018, 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 "chef/exceptions"
-
-class Chef
- class DataCollector
- class ResourceReport
-
- attr_reader :action, :elapsed_time, :new_resource, :status
- attr_accessor :conditional, :current_resource, :exception
-
- def initialize(new_resource, action, current_resource = nil)
- @new_resource = new_resource
- @action = action
- @current_resource = current_resource
- @status = "unprocessed"
- end
-
- def skipped(conditional)
- @status = "skipped"
- @conditional = conditional
- end
-
- def updated
- @status = "updated"
- end
-
- def failed(exception)
- @current_resource = nil
- @status = "failed"
- @exception = exception
- end
-
- def up_to_date
- @status = "up-to-date"
- end
-
- def finish
- @elapsed_time = new_resource.elapsed_time
- end
-
- def elapsed_time_in_milliseconds
- elapsed_time.nil? ? nil : (elapsed_time * 1000).to_i
- end
-
- def potentially_changed?
- %w{updated failed}.include?(status)
- end
-
- def to_h
- hash = {
- "type" => new_resource.resource_name.to_sym,
- "name" => new_resource.name.to_s,
- "id" => resource_identity,
- "after" => new_resource_state_reporter,
- "before" => current_resource_state_reporter,
- "duration" => elapsed_time_in_milliseconds.to_s,
- "delta" => new_resource.respond_to?(:diff) && potentially_changed? ? new_resource.diff : "",
- "ignore_failure" => new_resource.ignore_failure,
- "result" => action.to_s,
- "status" => status,
- }
-
- if new_resource.cookbook_name
- hash["cookbook_name"] = new_resource.cookbook_name
- hash["cookbook_version"] = new_resource.cookbook_version.version
- hash["recipe_name"] = new_resource.recipe_name
- end
-
- hash["conditional"] = conditional.to_text if status == "skipped"
- hash["error_message"] = exception.message unless exception.nil?
-
- hash
- end
- alias_method :to_hash, :to_h
- alias_method :for_json, :to_h
-
- # We should be able to call the identity of a resource safely, but there
- # is an edge case where resources that have a lazy property that is both
- # the name_property and the identity property, it will thow a validation
- # exception causing the chef-client run to fail. We are not fixing this
- # case since Chef is actually doing the right thing but we are making the
- # ResourceReporter smarter so that it detects the failure and sends a
- # message to the data collector containing a static resource identity
- # since we were unable to generate a proper one.
- def resource_identity
- new_resource.identity.to_s
- rescue => e
- "unknown identity (due to #{e.class})"
- end
-
- def new_resource_state_reporter
- new_resource.state_for_resource_reporter
- rescue
- {}
- end
-
- def current_resource_state_reporter
- current_resource ? current_resource.state_for_resource_reporter : {}
- rescue
- {}
- end
- end
- end
-end
diff --git a/lib/chef/data_collector/run_end_message.rb b/lib/chef/data_collector/run_end_message.rb
new file mode 100644
index 0000000000..1aba651d96
--- /dev/null
+++ b/lib/chef/data_collector/run_end_message.rb
@@ -0,0 +1,172 @@
+#
+# Copyright:: Copyright 2012-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 "chef/data_collector/message_helpers"
+
+class Chef
+ class DataCollector
+ module RunEndMessage
+ extend Chef::DataCollector::MessageHelpers
+
+ # This module encapsulates rendering the run_end_message given the state gathered in the data_collector
+ # and the action_collection. It is deliberately a stateless module and is deliberately not mixed into
+ # the data_collector and only uses the public api methods of the data_collector and action_collection.
+ #
+ # No external code should call this module directly.
+ #
+ # @api private
+ class << self
+
+ # Construct the message payload that is sent to the DataCollector server at the
+ # end of a Chef run.
+ #
+ # @param data_collector [Chef::DataCollector::Reporter] the calling data_collector instance
+ # @param status [String] the overall status of the run, either "success" or "failure"
+ #
+ # @return [Hash] A hash containing the run end message data.
+ #
+ def construct_message(data_collector, status)
+ action_collection = data_collector.action_collection
+ run_status = data_collector.run_status
+ node = data_collector.node
+
+ message = {
+ "chef_server_fqdn" => URI(Chef::Config[:chef_server_url]).host,
+ "entity_uuid" => Chef::Config[:chef_guid],
+ "expanded_run_list" => data_collector.expanded_run_list,
+ "id" => run_status&.run_id,
+ "message_version" => "1.1.0",
+ "message_type" => "run_converge",
+ "node" => node || {},
+ "node_name" => node&.name || data_collector.node_name,
+ "organization_name" => organization,
+ "resources" => all_action_records(action_collection),
+ "run_id" => run_status&.run_id,
+ "run_list" => node&.run_list&.for_json || [],
+ "policy_name" => node&.policy_name,
+ "policy_group" => node&.policy_group,
+ "start_time" => run_status.start_time.utc.iso8601,
+ "end_time" => run_status.end_time.utc.iso8601,
+ "source" => solo_run? ? "chef_solo" : "chef_client",
+ "status" => status,
+ "total_resource_count" => all_action_records(action_collection).count,
+ "updated_resource_count" => updated_resource_count(action_collection),
+ "deprecations" => data_collector.deprecations.to_a,
+ }
+
+ if run_status&.exception
+ message["error"] = {
+ "class" => run_status.exception.class,
+ "message" => run_status.exception.message,
+ "backtrace" => run_status.exception.backtrace,
+ "description" => data_collector.error_description,
+ }
+ end
+
+ message
+ end
+
+ private
+
+ # @return [Integer] the number of resources successfully updated in the chef-client run
+ def updated_resource_count(action_collection)
+ return 0 if action_collection.nil?
+ action_collection.filtered_collection(up_to_date: false, skipped: false, unprocessed: false, failed: false).count
+ end
+
+ # @return [Array<Chef::ActionCollection::ActionRecord>] list of all action_records for all resources
+ def action_records(action_collection)
+ return [] if action_collection.nil?
+ action_collection.action_records
+ end
+
+ # @return [Array<Hash>] list of all action_records rendered as a Hash for sending to JSON
+ def all_action_records(action_collection)
+ action_records(action_collection).map { |rec| action_record_for_json(rec) }
+ end
+
+ # @return [Hash] the Hash representation of the action_record for sending as JSON
+ def action_record_for_json(action_record)
+ new_resource = action_record.new_resource
+ current_resource = action_record.current_resource
+
+ hash = {
+ "type" => new_resource.resource_name.to_sym,
+ "name" => new_resource.name.to_s,
+ "id" => safe_resource_identity(new_resource),
+ "after" => safe_state_for_resource_reporter(new_resource),
+ "before" => safe_state_for_resource_reporter(current_resource),
+ "duration" => action_record.elapsed_time.nil? ? "" : (action_record.elapsed_time * 1000).to_i.to_s,
+ "delta" => new_resource.respond_to?(:diff) && updated_or_failed?(action_record) ? new_resource.diff : "",
+ "ignore_failure" => new_resource.ignore_failure,
+ "result" => action_record.action.to_s,
+ "status" => action_record_status_for_json(action_record),
+ }
+
+ if new_resource.cookbook_name
+ hash["cookbook_name"] = new_resource.cookbook_name
+ hash["cookbook_version"] = new_resource.cookbook_version.version
+ hash["recipe_name"] = new_resource.recipe_name
+ end
+
+ hash["conditional"] = action_record.conditional.to_text if action_record.status == :skipped
+ hash["error_message"] = action_record.exception.message unless action_record.exception.nil?
+
+ hash
+ end
+
+ # If the identity property of a resource has been lazied (via a lazy name resource) evaluating it
+ # for an unprocessed resource (where the preconditions have not been met) may cause the lazy
+ # evaluator to throw -- and would otherwise crash the data collector.
+ #
+ # @return [String] the resource's identity property
+ #
+ def safe_resource_identity(new_resource)
+ new_resource.identity.to_s
+ rescue => e
+ "unknown identity (due to #{e.class})"
+ end
+
+ # FIXME: This is likely necessary due to the same lazy issue with properties and failing resources?
+ #
+ # @return [Hash] the resource's reported state properties
+ #
+ def safe_state_for_resource_reporter(resource)
+ resource ? resource.state_for_resource_reporter : {}
+ rescue
+ {}
+ end
+
+ # Helper to convert action record status (symbols) to strings for the Data Collector server.
+ # Does a bit of necessary underscores-to-dashes conversion to comply with the Data Collector API.
+ #
+ # @return [String] resource status (
+ #
+ def action_record_status_for_json(action_record)
+ action = action_record.status.to_s
+ action = "up-to-date" if action == "up_to_date"
+ action
+ end
+
+ # @return [Boolean] True if the resource was updated or failed
+ def updated_or_failed?(action_record)
+ action_record.status == :updated || action_record.status == :failed
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/data_collector/run_start_message.rb b/lib/chef/data_collector/run_start_message.rb
new file mode 100644
index 0000000000..448043a48d
--- /dev/null
+++ b/lib/chef/data_collector/run_start_message.rb
@@ -0,0 +1,60 @@
+#
+# Copyright:: Copyright 2012-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 "chef/data_collector/message_helpers"
+
+class Chef
+ class DataCollector
+ module RunStartMessage
+ extend Chef::DataCollector::MessageHelpers
+
+ # This module encapsulates rendering the run_start_message given the state gathered in the data_collector.
+ # It is deliberately a stateless module and is deliberately not mixed into the data_collector and only
+ # uses the public api methods of the data_collector.
+ #
+ # No external code should call this module directly.
+ #
+ # @api private
+ class << self
+
+ # Construct the message payload that is sent to the DataCollector server at the
+ # start of a Chef run.
+ #
+ # @param data_collector [Chef::DataCollector::Reporter] the calling data_collector instance
+ #
+ # @return [Hash] A hash containing the run start message data.
+ #
+ def construct_message(data_collector)
+ run_status = data_collector.run_status
+ node = data_collector.node
+ {
+ "chef_server_fqdn" => URI(Chef::Config[:chef_server_url]).host,
+ "entity_uuid" => Chef::Config[:chef_guid],
+ "id" => run_status&.run_id,
+ "message_version" => "1.0.0",
+ "message_type" => "run_start",
+ "node_name" => node&.name || data_collector.node_name,
+ "organization_name" => organization,
+ "run_id" => run_status&.run_id,
+ "source" => solo_run? ? "chef_solo" : "chef_client",
+ "start_time" => run_status.start_time.utc.iso8601,
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/event_dispatch/base.rb b/lib/chef/event_dispatch/base.rb
index 0886d63152..3b0b70c9b9 100644
--- a/lib/chef/event_dispatch/base.rb
+++ b/lib/chef/event_dispatch/base.rb
@@ -29,7 +29,7 @@ class Chef
class Base
# Called at the very start of a Chef Run
- def run_start(version)
+ def run_start(version, run_status)
end
def run_started(run_status)
@@ -44,6 +44,7 @@ class Chef
end
# Called right after ohai runs.
+ # NOTE: the node object here is always nil because of when it is called
def ohai_completed(node)
end
@@ -73,6 +74,10 @@ class Chef
# TODO: def node_run_list_overridden(*args)
+ # Called once the node is loaded by the policy builder
+ def node_load_success(node)
+ end
+
# Failed to load node data from the server
def node_load_failed(node_name, exception, config)
end
@@ -163,6 +168,10 @@ class Chef
## TODO: add callbacks for overall cookbook eval start and complete.
+ # Called immediately after creating the run_context and before any cookbook compilation happens
+ def cookbook_compilation_start(run_context)
+ end
+
# Called when library file loading starts
def library_load_start(file_count)
end
@@ -263,10 +272,18 @@ class Chef
def recipe_load_complete
end
+ # This is called after all cookbook compilation phases are completed.
+ def cookbook_compilation_complete(run_context)
+ end
+
# Called before convergence starts
def converge_start(run_context)
end
+ # Callback hook for handlers to register their interest in the action_collection
+ def action_collection_registration(action_collection)
+ end
+
# Called when the converge phase is finished.
def converge_complete
end
diff --git a/lib/chef/event_dispatch/dispatcher.rb b/lib/chef/event_dispatch/dispatcher.rb
index 69419a393b..d69a7c76e8 100644
--- a/lib/chef/event_dispatch/dispatcher.rb
+++ b/lib/chef/event_dispatch/dispatcher.rb
@@ -10,19 +10,44 @@ class Chef
class Dispatcher < Base
attr_reader :subscribers
+ attr_reader :event_list
def initialize(*subscribers)
@subscribers = subscribers
+ @event_list = []
end
# Add a new subscriber to the list of registered subscribers
def register(subscriber)
- @subscribers << subscriber
+ subscribers << subscriber
+ end
+
+ def unregister(subscriber)
+ subscribers.reject! { |x| x == subscriber }
+ end
+
+ def enqueue(method_name, *args)
+ event_list << [ method_name, *args ]
+ process_events_until_done unless @in_call
+ end
+
+ (Base.instance_methods - Object.instance_methods).each do |method_name|
+ class_eval <<-EOM
+ def #{method_name}(*args)
+ enqueue(#{method_name.inspect}, *args)
+ end
+ EOM
+ end
+
+ # Special case deprecation, since it needs to know its caller
+ def deprecation(message, location = caller(2..2)[0])
+ enqueue(:deprecation, message, location)
end
# Check to see if we are dispatching to a formatter
+ # @api private
def formatter?
- @subscribers.any? { |s| s.respond_to?(:is_formatter?) && s.is_formatter? }
+ subscribers.any? { |s| s.respond_to?(:is_formatter?) && s.is_formatter? }
end
####
@@ -30,9 +55,11 @@ class Chef
# define the forwarding in one go:
#
+ # @api private
def call_subscribers(method_name, *args)
- @subscribers.each do |s|
- # Skip new/unsupported event names.
+ @in_call = true
+ subscribers.each do |s|
+ # Skip new/unsupported event names
next if !s.respond_to?(method_name)
mth = s.method(method_name)
# Trim arguments to match what the subscriber expects to allow
@@ -43,20 +70,19 @@ class Chef
mth.call(*args)
end
end
+ ensure
+ @in_call = false
end
- (Base.instance_methods - Object.instance_methods).each do |method_name|
- class_eval <<-EOM
- def #{method_name}(*args)
- call_subscribers(#{method_name.inspect}, *args)
- end
- EOM
- end
+ private
- # Special case deprecation, since it needs to know its caller
- def deprecation(message, location = caller(2..2)[0])
- call_subscribers(:deprecation, message, location)
+ # events are allowed to enqueue chained events, so pop them off until
+ # empty, rather than iterating over the list.
+ #
+ def process_events_until_done
+ call_subscribers(*event_list.shift) until event_list.empty?
end
+
end
end
end
diff --git a/lib/chef/event_loggers/windows_eventlog.rb b/lib/chef/event_loggers/windows_eventlog.rb
index 950477556a..e084fac684 100644
--- a/lib/chef/event_loggers/windows_eventlog.rb
+++ b/lib/chef/event_loggers/windows_eventlog.rb
@@ -1,7 +1,7 @@
#
# Author:: Jay Mundrawala (<jdm@chef.io>)
#
-# Copyright:: Copyright 2014-2016, Chef Software, Inc.
+# Copyright:: Copyright 2014-2019, Chef Software Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -45,7 +45,7 @@ class Chef
@eventlog = ::Win32::EventLog.open("Application")
end
- def run_start(version)
+ def run_start(version, run_status)
@eventlog.report_event(
event_type: ::Win32::EventLog::INFO_TYPE,
source: SOURCE,
diff --git a/lib/chef/formatters/doc.rb b/lib/chef/formatters/doc.rb
index 19a6fafae6..936738f147 100644
--- a/lib/chef/formatters/doc.rb
+++ b/lib/chef/formatters/doc.rb
@@ -42,7 +42,7 @@ class Chef
message
end
- def run_start(version)
+ def run_start(version, run_status)
puts_line "Starting Chef Client, version #{version}"
puts_line "OpenSSL FIPS 140 mode enabled" if Chef::Config[:fips]
end
diff --git a/lib/chef/formatters/minimal.rb b/lib/chef/formatters/minimal.rb
index c8fc504eb0..e182189f7d 100644
--- a/lib/chef/formatters/minimal.rb
+++ b/lib/chef/formatters/minimal.rb
@@ -26,7 +26,7 @@ class Chef
end
# Called at the very start of a Chef Run
- def run_start(version)
+ def run_start(version, run_status)
puts_line "Starting Chef Client, version #{version}"
puts_line "OpenSSL FIPS 140 mode enabled" if Chef::Config[:fips]
end
diff --git a/lib/chef/node.rb b/lib/chef/node.rb
index 87418b5732..9b67f27f14 100644
--- a/lib/chef/node.rb
+++ b/lib/chef/node.rb
@@ -2,7 +2,7 @@
# Author:: Christopher Brown (<cb@chef.io>)
# Author:: Christopher Walters (<cw@chef.io>)
# Author:: Tim Hinderliter (<tim@chef.io>)
-# Copyright:: Copyright 2008-2018, Chef Software Inc.
+# Copyright:: Copyright 2008-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,6 +19,7 @@
#
require "forwardable"
+require "securerandom"
require "chef/config"
require "chef/nil_argument"
require "chef/mixin/params_validate"
@@ -338,7 +339,7 @@ class Chef
logger.debug("Platform is #{platform} version #{version}")
automatic[:platform] = platform
automatic[:platform_version] = version
- automatic[:chef_guid] = Chef::Config[:chef_guid]
+ automatic[:chef_guid] = Chef::Config[:chef_guid] || ( Chef::Config[:chef_guid] = node_uuid )
automatic[:name] = name
automatic[:chef_environment] = chef_environment
end
@@ -687,5 +688,24 @@ class Chef
data
end
+ # Returns a UUID that uniquely identifies this node for reporting reasons.
+ #
+ # The node is read in from disk if it exists, or it's generated if it does
+ # does not exist.
+ #
+ # @return [String] UUID for the node
+ #
+ def node_uuid
+ path = File.expand_path(Chef::Config[:chef_guid_path])
+ dir = File.dirname(path)
+
+ unless File.exists?(path)
+ FileUtils.mkdir_p(dir)
+ File.write(path, SecureRandom.uuid)
+ end
+
+ File.open(path).first.chomp
+ end
+
end
end
diff --git a/lib/chef/policy_builder/dynamic.rb b/lib/chef/policy_builder/dynamic.rb
index 8ce4f25bfa..2c49f41656 100644
--- a/lib/chef/policy_builder/dynamic.rb
+++ b/lib/chef/policy_builder/dynamic.rb
@@ -1,6 +1,6 @@
#
# Author:: Daniel DeLeo (<dan@chef.io>)
-# Copyright:: Copyright 2015-2016, Chef Software, Inc.
+# Copyright:: Copyright 2015-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -74,6 +74,7 @@ class Chef
select_implementation(node)
implementation.finish_load_node(node)
node
+ events.node_load_success(node)
rescue Exception => e
events.node_load_failed(node_name, e, config)
raise
diff --git a/lib/chef/policy_builder/expand_node_object.rb b/lib/chef/policy_builder/expand_node_object.rb
index c91c74b047..839c3bb526 100644
--- a/lib/chef/policy_builder/expand_node_object.rb
+++ b/lib/chef/policy_builder/expand_node_object.rb
@@ -3,7 +3,7 @@
# Author:: Tim Hinderliter (<tim@chef.io>)
# Author:: Christopher Walters (<cw@chef.io>)
# Author:: Daniel DeLeo (<dan@chef.io>)
-# Copyright:: Copyright 2008-2017, Chef Software Inc.
+# Copyright:: Copyright 2008-2018, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -68,6 +68,11 @@ class Chef
Chef.set_run_context(run_context)
end
+ # This not only creates the run_context but this is where we kick off
+ # compiling the entire expanded run_list, loading all the libraries, resources,
+ # attribute files and recipes, and constructing the entire resource collection.
+ # (FIXME: break up creating the run_context and compiling the cookbooks)
+ #
def setup_run_context(specific_recipes = nil)
if Chef::Config[:solo_legacy_mode]
Chef::Cookbook::FileVendor.fetch_from_disk(Chef::Config[:cookbook_path])
@@ -88,18 +93,20 @@ class Chef
run_context = Chef::RunContext.new(node, cookbook_collection, @events)
end
- # TODO: this is really obviously not the place for this
- # FIXME: need same edits
+ # TODO: move this into the cookbook_compilation_start hook
setup_chef_class(run_context)
- # TODO: this is not the place for this. It should be in Runner or
- # CookbookCompiler or something.
+ events.cookbook_compilation_start(run_context)
+
run_context.load(@run_list_expansion)
if specific_recipes
specific_recipes.each do |recipe_file|
run_context.load_recipe_file(recipe_file)
end
end
+
+ events.cookbook_compilation_complete(run_context)
+
run_context
end
diff --git a/lib/chef/policy_builder/policyfile.rb b/lib/chef/policy_builder/policyfile.rb
index 3e7462f0ed..bedbe651a1 100644
--- a/lib/chef/policy_builder/policyfile.rb
+++ b/lib/chef/policy_builder/policyfile.rb
@@ -192,8 +192,12 @@ class Chef
setup_chef_class(run_context)
+ events.cookbook_compilation_start(run_context)
+
run_context.load(run_list_expansion_ish)
+ events.cookbook_compilation_complete(run_context)
+
setup_chef_class(run_context)
run_context
end
diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb
index c857d76c02..e08b2b0bc3 100644
--- a/lib/chef/resource.rb
+++ b/lib/chef/resource.rb
@@ -140,6 +140,7 @@ class Chef
@guard_interpreter = nil
@default_guard_interpreter = :default
@elapsed_time = 0
+ @executed_by_runner = false
end
#
@@ -453,6 +454,11 @@ class Chef
attr_reader :elapsed_time
#
+ # @return [Boolean] If the resource was executed by the runner
+ #
+ attr_accessor :executed_by_runner
+
+ #
# The guard interpreter that will be used to process `only_if` and `not_if`
# statements. If left unset, the #default_guard_interpreter will be used.
#
@@ -1184,8 +1190,10 @@ class Chef
# Internal Resource Interface (for Chef)
#
- FORBIDDEN_IVARS = [:@run_context, :@logger, :@not_if, :@only_if, :@enclosing_provider, :@description, :@introduced, :@examples, :@validation_message, :@deprecated, :@default_description, :@skip_docs].freeze
- HIDDEN_IVARS = [:@allowed_actions, :@resource_name, :@source_line, :@run_context, :@logger, :@name, :@not_if, :@only_if, :@elapsed_time, :@enclosing_provider, :@description, :@introduced, :@examples, :@validation_message, :@deprecated, :@default_description, :@skip_docs].freeze
+ # FORBIDDEN_IVARS do not show up when the resource is converted to JSON (ie. hidden from data_collector and sending to the chef server via #to_json/to_h/as_json/inspect)
+ FORBIDDEN_IVARS = [:@run_context, :@logger, :@not_if, :@only_if, :@enclosing_provider, :@description, :@introduced, :@examples, :@validation_message, :@deprecated, :@default_description, :@skip_docs, :@executed_by_runner].freeze
+ # HIDDEN_IVARS do not show up when the resource is displayed to the user as text (ie. in the error inspector output via #to_text)
+ HIDDEN_IVARS = [:@allowed_actions, :@resource_name, :@source_line, :@run_context, :@logger, :@name, :@not_if, :@only_if, :@elapsed_time, :@enclosing_provider, :@description, :@introduced, :@examples, :@validation_message, :@deprecated, :@default_description, :@skip_docs, :@executed_by_runner].freeze
include Chef::Mixin::ConvertToClassName
extend Chef::Mixin::ConvertToClassName
diff --git a/lib/chef/resource_collection/resource_list.rb b/lib/chef/resource_collection/resource_list.rb
index 75305f63a6..3c993d6e55 100644
--- a/lib/chef/resource_collection/resource_list.rb
+++ b/lib/chef/resource_collection/resource_list.rb
@@ -1,6 +1,6 @@
#
# Author:: Tyler Ball (<tball@chef.io>)
-# Copyright:: Copyright 2014-2016, Chef Software, Inc.
+# Copyright:: Copyright 2014-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/lib/chef/resource_reporter.rb b/lib/chef/resource_reporter.rb
index 3198f84d12..01e4073549 100644
--- a/lib/chef/resource_reporter.rb
+++ b/lib/chef/resource_reporter.rb
@@ -3,7 +3,7 @@
# Author:: Prajakta Purohit (prajakta@chef.io>)
# Auther:: Tyler Cloke (<tyler@opscode.com>)
#
-# Copyright:: Copyright 2012-2018, Chef Software Inc.
+# Copyright:: Copyright 2012-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,88 +25,40 @@ require "chef/event_dispatch/base"
class Chef
class ResourceReporter < EventDispatch::Base
+ def for_json(action_record)
+ new_resource = action_record.new_resource
+ current_resource = action_record.current_resource
- ResourceReport = Struct.new(:new_resource,
- :current_resource,
- :action,
- :exception,
- :elapsed_time) do
-
- def self.new_with_current_state(new_resource, action, current_resource)
- report = new
- report.new_resource = new_resource
- report.action = action
- report.current_resource = current_resource
- report
- end
+ as_hash = {}
+ as_hash["type"] = new_resource.resource_name.to_sym
+ as_hash["name"] = new_resource.name.to_s
+ as_hash["id"] = new_resource.identity.to_s
+ as_hash["after"] = new_resource.state_for_resource_reporter
+ as_hash["before"] = current_resource ? current_resource.state_for_resource_reporter : {}
+ as_hash["duration"] = ( action_record.elapsed_time * 1000 ).to_i
+ as_hash["delta"] = new_resource.diff if new_resource.respond_to?("diff")
+ as_hash["delta"] = "" if as_hash["delta"].nil?
- def self.new_for_exception(new_resource, action)
- report = new
- report.new_resource = new_resource
- report.action = action
- report
+ # TODO: rename as "action"
+ as_hash["result"] = action_record.action.to_s
+ if new_resource.cookbook_name
+ as_hash["cookbook_name"] = new_resource.cookbook_name
+ as_hash["cookbook_version"] = new_resource.cookbook_version.version
end
- # Future: Some resources store state information that does not convert nicely
- # to json. We can't call a resource's state method here, since there are conflicts
- # with some LWRPs, so we can't override a resource's state method to return
- # json-friendly state data.
- #
- # The registry key resource returns json-friendly state data through its state
- # attribute, and uses a read-only variable for fetching true state data. If
- # we have conflicts with other resources reporting json incompatible state, we
- # may want to extend the state_attrs API with the ability to rename POST'd
- # attrs.
- def for_json
- as_hash = {}
- as_hash["type"] = new_resource.resource_name.to_sym
- as_hash["name"] = new_resource.name.to_s
- as_hash["id"] = new_resource.identity.to_s
- as_hash["after"] = new_resource.state_for_resource_reporter
- as_hash["before"] = current_resource ? current_resource.state_for_resource_reporter : {}
- as_hash["duration"] = (elapsed_time * 1000).to_i.to_s
- as_hash["delta"] = new_resource.diff if new_resource.respond_to?("diff")
- as_hash["delta"] = "" if as_hash["delta"].nil?
-
- # TODO: rename as "action"
- as_hash["result"] = action.to_s
- if success?
- else
- # as_hash["result"] = "failed"
- end
- if new_resource.cookbook_name
- as_hash["cookbook_name"] = new_resource.cookbook_name
- as_hash["cookbook_version"] = new_resource.cookbook_version.version
- end
-
- as_hash
- end
-
- def finish
- self.elapsed_time = new_resource.elapsed_time
- end
-
- def success?
- !exception
- end
- end # End class ResouceReport
+ as_hash
+ end
- attr_reader :updated_resources
attr_reader :status
attr_reader :exception
attr_reader :error_descriptions
+ attr_reader :action_collection
+ attr_reader :rest_client
PROTOCOL_VERSION = "0.1.0".freeze
def initialize(rest_client)
- if Chef::Config[:enable_reporting] && !Chef::Config[:why_run]
- @reporting_enabled = true
- else
- @reporting_enabled = false
- end
- @updated_resources = []
- @total_res_count = 0
- @pending_update = nil
+ @pending_update = nil
@status = "success"
@exception = nil
@rest_client = rest_client
@@ -120,8 +72,8 @@ class Chef
if reporting_enabled?
begin
resource_history_url = "reports/nodes/#{node_name}/runs"
- server_response = @rest_client.post(resource_history_url, { action: :start, run_id: run_id,
- start_time: start_time.to_s }, headers)
+ server_response = rest_client.post(resource_history_url, { action: :start, run_id: run_id,
+ start_time: start_time.to_s }, headers)
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e
handle_error_starting_run(e, resource_history_url)
end
@@ -157,62 +109,13 @@ class Chef
end
end
- @reporting_enabled = false
+ @runs_endpoint_failed = true
end
def run_id
@run_status.run_id
end
- def resource_current_state_loaded(new_resource, action, current_resource)
- unless nested_resource?(new_resource)
- @pending_update = ResourceReport.new_with_current_state(new_resource, action, current_resource)
- end
- end
-
- def resource_up_to_date(new_resource, action)
- @total_res_count += 1
- @pending_update = nil unless nested_resource?(new_resource)
- end
-
- def resource_skipped(resource, action, conditional)
- @total_res_count += 1
- @pending_update = nil unless nested_resource?(resource)
- end
-
- def resource_updated(new_resource, action)
- @total_res_count += 1
- end
-
- def resource_failed(new_resource, action, exception)
- @total_res_count += 1
- unless nested_resource?(new_resource)
- @pending_update ||= ResourceReport.new_for_exception(new_resource, action)
- @pending_update.exception = exception
- end
- description = Formatters::ErrorMapper.resource_failed(new_resource, action, exception)
- @error_descriptions = description.for_json
- end
-
- def resource_completed(new_resource)
- if @pending_update && !nested_resource?(new_resource)
- @pending_update.finish
-
- # 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
- if @pending_update.new_resource.sensitive
- klass = @pending_update.new_resource.class
- resource_name = @pending_update.new_resource.name
- @pending_update.new_resource = klass.new(resource_name)
- end
-
- @updated_resources << @pending_update
- @pending_update = nil
- end
- end
-
def run_completed(node)
@status = "success"
post_reporting_data
@@ -232,6 +135,11 @@ class Chef
@expanded_run_list = run_list_expansion
end
+ def action_collection_registration(action_collection)
+ @action_collection = action_collection
+ action_collection.register(self) if reporting_enabled?
+ end
+
def post_reporting_data
if reporting_enabled?
run_data = prepare_run_data
@@ -242,7 +150,7 @@ class Chef
Chef::Log.trace("Sending compressed run data...")
# Since we're posting compressed data we can not directly call post which expects JSON
begin
- @rest_client.raw_request(:POST, resource_history_url, headers({ "Content-Encoding" => "gzip" }), compressed_data)
+ rest_client.raw_request(:POST, resource_history_url, headers({ "Content-Encoding" => "gzip" }), compressed_data)
rescue StandardError => e
if e.respond_to? :response
Chef::FileCache.store("failed-reporting-data.json", Chef::JSONCompat.to_json_pretty(run_data), 0640)
@@ -273,15 +181,24 @@ class Chef
@run_status.end_time
end
+ # get only the top level resources and strip out the subcollections
+ def updated_resources
+ @updated_resources ||= action_collection.filtered_collection(max_nesting: 0, up_to_date: false, skipped: false, unprocessed: false)
+ end
+
+ def total_res_count
+ updated_resources.count
+ end
+
def prepare_run_data
run_data = {}
run_data["action"] = "end"
- run_data["resources"] = updated_resources.map do |resource_record|
- resource_record.for_json
+ run_data["resources"] = updated_resources.map do |action_record|
+ for_json(action_record)
end
run_data["status"] = @status
run_data["run_list"] = Chef::JSONCompat.to_json(@run_status.node.run_list)
- run_data["total_res_count"] = @total_res_count.to_s
+ run_data["total_res_count"] = total_res_count.to_s
run_data["data"] = {}
run_data["start_time"] = start_time.to_s
run_data["end_time"] = end_time.to_s
@@ -313,18 +230,10 @@ class Chef
@error_descriptions = description.for_json
end
- def reporting_enabled?
- @reporting_enabled
- end
-
private
- # If we are getting messages about a resource while we are in the middle of
- # another resource's update, we assume that the nested resource is just the
- # implementation of a provider, and we want to hide it from the reporting
- # output.
- def nested_resource?(new_resource)
- @pending_update && @pending_update.new_resource != new_resource
+ def reporting_enabled?
+ Chef::Config[:enable_reporting] && !Chef::Config[:why_run] && !@runs_endpoint_failed
end
def encode_gzip(data)
diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb
index b940bfecbd..e407a0e7be 100644
--- a/lib/chef/run_context.rb
+++ b/lib/chef/run_context.rb
@@ -2,7 +2,7 @@
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Christopher Walters (<cw@chef.io>)
# Author:: Tim Hinderliter (<tim@chef.io>)
-# Copyright:: Copyright 2008-2017, Chef Software Inc.
+# Copyright:: Copyright 2008-2018, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -104,6 +104,7 @@ class Chef
#
attr_reader :resource_collection
+ attr_accessor :action_collection
#
# The list of control groups to execute during the audit phase
#
@@ -603,6 +604,8 @@ class Chef
class ChildRunContext < RunContext
extend Forwardable
def_delegators :parent_run_context, *%w{
+ action_collection
+ action_collection=
cancel_reboot
config
cookbook_collection
diff --git a/lib/chef/run_context/cookbook_compiler.rb b/lib/chef/run_context/cookbook_compiler.rb
index c3cee5841f..1bd612e721 100644
--- a/lib/chef/run_context/cookbook_compiler.rb
+++ b/lib/chef/run_context/cookbook_compiler.rb
@@ -1,6 +1,6 @@
#
# Author:: Daniel DeLeo (<dan@chef.io>)
-# Copyright:: Copyright 2012-2018, Chef Software Inc.
+# Copyright:: Copyright 2012-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/lib/chef/run_status.rb b/lib/chef/run_status.rb
index 37b10fb9be..575d31159b 100644
--- a/lib/chef/run_status.rb
+++ b/lib/chef/run_status.rb
@@ -1,6 +1,6 @@
#
# Author:: Daniel DeLeo (<dan@chef.io>)
-# Copyright:: Copyright 2010-2018, Chef Software Inc.
+# Copyright:: Copyright 2010-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -53,6 +53,7 @@ class Chef::RunStatus
# sets +end_time+ to the current time
def stop_clock
+ @start_time ||= Time.now # if we failed so early we didn't get a start time
@end_time = Time.now
end
diff --git a/lib/chef/runner.rb b/lib/chef/runner.rb
index 1c82439b57..d5dab75dfd 100644
--- a/lib/chef/runner.rb
+++ b/lib/chef/runner.rb
@@ -2,7 +2,7 @@
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Christopher Walters (<cw@chef.io>)
# Author:: Tim Hinderliter (<tim@chef.io>)
-# Copyright:: Copyright 2008-2017, Chef Software Inc.
+# Copyright:: Copyright 2008-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -95,7 +95,11 @@ class Chef
# Execute each resource.
run_context.resource_collection.execute_each_resource do |resource|
- Array(resource.action).each { |action| run_action(resource, action) }
+ begin
+ Array(resource.action).each { |action| run_action(resource, action) }
+ ensure
+ resource.executed_by_runner = true
+ end
end
rescue Exception => e
diff --git a/spec/functional/event_loggers/windows_eventlog_spec.rb b/spec/functional/event_loggers/windows_eventlog_spec.rb
index 8a9475680d..fa84e96c25 100644
--- a/spec/functional/event_loggers/windows_eventlog_spec.rb
+++ b/spec/functional/event_loggers/windows_eventlog_spec.rb
@@ -1,7 +1,7 @@
#
# Author:: Jay Mundrawala (<jdm@chef.io>)
#
-# Copyright:: Copyright 2014-2016, Chef Software, Inc.
+# Copyright:: Copyright 2014-2019, Chef Software Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -46,7 +46,7 @@ describe Chef::EventLoggers::WindowsEventLogger, :windows_only do
end
it "writes run_start event with event_id 10000 and contains version" do
- logger.run_start(version)
+ logger.run_start(version, run_status)
expect(event_log.read(flags, offset).any? do |e|
e.source == "Chef" && e.event_id == 10000 &&
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index c493987554..d13453b778 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,6 +1,6 @@
#
# Author:: Adam Jacob (<adam@chef.io>)
-# Copyright:: Copyright 2008-2018, Chef Software Inc.
+# Copyright:: Copyright 2008-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/spec/support/shared/context/client.rb b/spec/support/shared/context/client.rb
deleted file mode 100644
index d6c49c896b..0000000000
--- a/spec/support/shared/context/client.rb
+++ /dev/null
@@ -1,305 +0,0 @@
-
-require "spec_helper"
-
-# Stubs a basic client object
-shared_context "client" do
- let(:fqdn) { "hostname.example.org" }
- let(:hostname) { "hostname" }
- let(:machinename) { "machinename.example.org" }
- let(:platform) { "example-platform" }
- let(:platform_version) { "example-platform-1.0" }
-
- let(:ohai_data) do
- {
- fqdn: fqdn,
- hostname: hostname,
- machinename: machinename,
- platform: platform,
- platform_version: platform_version,
- }
- end
-
- let(:ohai_system) do
- ohai = instance_double("Ohai::System", all_plugins: true, data: ohai_data, logger: logger)
- allow(ohai).to receive(:[]) do |k|
- ohai_data[k]
- end
- ohai
- end
-
- let(:node) do
- Chef::Node.new.tap do |n|
- n.name(fqdn)
- n.chef_environment("_default")
- end
- end
-
- let(:json_attribs) { nil }
- let(:client_opts) { {} }
-
- let(:stdout) { STDOUT }
- let(:stderr) { STDERR }
-
- let(:client) do
- Chef::Config[:event_loggers] = []
- allow(Ohai::System).to receive(:new).and_return(ohai_system)
- opts = client_opts.merge({ logger: logger })
- Chef::Client.new(json_attribs, opts).tap do |c|
- c.node = node
- end
- end
-
- let(:logger) { instance_double("Mixlib::Log::Child", trace: nil, debug: nil, warn: nil, info: nil, error: nil, fatal: nil) }
-
- before do
- stub_const("Chef::Client::STDOUT_FD", stdout)
- stub_const("Chef::Client::STDERR_FD", stderr)
- allow(client).to receive(:logger).and_return(logger)
- end
-end
-
-# Stubs a client for a client run.
-# Requires a client object be defined in the scope of this included context.
-# e.g.:
-# describe "some functionality" do
-# include_context "client"
-# include_context "a client run"
-# ...
-# end
-shared_context "a client run" do
- let(:stdout) { StringIO.new }
- let(:stderr) { StringIO.new }
-
- let(:api_client_exists?) { false }
- let(:enable_fork) { false }
-
- let(:http_data_collector) { double("Chef::ServerAPI (data collector)") }
- let(:http_cookbook_sync) { double("Chef::ServerAPI (cookbook sync)") }
- let(:http_node_load) { double("Chef::ServerAPI (node)") }
- let(:http_node_save) { double("Chef::ServerAPI (node save)") }
- let(:reporting_rest_client) { double("Chef::ServerAPI (reporting client)") }
-
- let(:runner) { instance_double("Chef::Runner") }
- let(:audit_runner) { instance_double("Chef::Audit::Runner", failed?: false) }
-
- def stub_for_register
- # --Client.register
- # Make sure Client#register thinks the client key doesn't
- # exist, so it tries to register and create one.
- allow(File).to receive(:exists?).and_call_original
- expect(File).to receive(:exists?)
- .with(Chef::Config[:client_key])
- .exactly(:once)
- .and_return(api_client_exists?)
-
- unless api_client_exists?
- # Client.register will register with the validation client name.
- expect_any_instance_of(Chef::ApiClient::Registration).to receive(:run)
- end
- end
-
- def stub_for_data_collector_init
- expect(Chef::ServerAPI).to receive(:new)
- .with(Chef::Config[:data_collector][:server_url], validate_utf8: false)
- .exactly(:once)
- .and_return(http_data_collector)
- end
-
- def stub_for_node_load
- # Client.register will then turn around create another
- # Chef::ServerAPI object, this time with the client key it got from the
- # previous step.
- expect(Chef::ServerAPI).to receive(:new)
- .with(Chef::Config[:chef_server_url], client_name: fqdn,
- signing_key_filename: Chef::Config[:client_key])
- .exactly(:once)
- .and_return(http_node_load)
-
- # --Client#build_node
- # looks up the node, which we will return, then later saves it.
- expect(Chef::Node).to receive(:find_or_create).with(fqdn).and_return(node)
-
- # --ResourceReporter#node_load_completed
- # gets a run id from the server for storing resource history
- # (has its own tests, so stubbing it here.)
- expect_any_instance_of(Chef::ResourceReporter).to receive(:node_load_completed)
- end
-
- def stub_rest_clean
- allow(client).to receive(:rest_clean).and_return(reporting_rest_client)
- end
-
- def stub_for_sync_cookbooks
- # --Client#setup_run_context
- # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync
- #
- expect_any_instance_of(Chef::CookbookSynchronizer).to receive(:sync_cookbooks)
- expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_url], version_class: Chef::CookbookManifestVersions).and_return(http_cookbook_sync)
- expect(http_cookbook_sync).to receive(:post)
- .with("environments/_default/cookbook_versions", { run_list: [] })
- .and_return({})
- end
-
- def stub_for_required_recipe
- response = Net::HTTPNotFound.new("1.1", "404", "Not Found")
- exception = Net::HTTPClientException.new('404 "Not Found"', response)
- expect(http_node_load).to receive(:get).with("required_recipe").and_raise(exception)
- end
-
- def stub_for_converge
- # define me
- end
-
- def stub_for_audit
- # define me
- end
-
- def stub_for_node_save
- # define me
- end
-
- def stub_for_run
- # define me
- end
-
- before do
- Chef::Config[:client_fork] = enable_fork
- Chef::Config[:cache_path] = windows? ? 'C:\chef' : "/var/chef"
- Chef::Config[:why_run] = false
- Chef::Config[:audit_mode] = :enabled
- Chef::Config[:chef_guid] = "default-guid"
-
- stub_rest_clean
- stub_for_register
- stub_for_data_collector_init
- stub_for_node_load
- stub_for_sync_cookbooks
- stub_for_required_recipe
- stub_for_converge
- stub_for_audit
- stub_for_node_save
-
- expect_any_instance_of(Chef::RunLock).to receive(:acquire)
- expect_any_instance_of(Chef::RunLock).to receive(:save_pid)
- expect_any_instance_of(Chef::RunLock).to receive(:release)
-
- # Post conditions: check that node has been filled in correctly
- expect(client).to receive(:run_started)
-
- stub_for_run
- end
-end
-
-shared_context "converge completed" do
- def stub_for_converge
- # --Client#converge
- expect(Chef::Runner).to receive(:new).and_return(runner)
- expect(runner).to receive(:converge).and_return(true)
- end
-
- def stub_for_node_save
- allow(node).to receive(:data_for_save).and_return(node.for_json)
-
- # --Client#save_updated_node
- expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_url], client_name: fqdn,
- signing_key_filename: Chef::Config[:client_key], validate_utf8: false).and_return(http_node_save)
- expect(http_node_save).to receive(:put).with("nodes/#{fqdn}", node.for_json).and_return(true)
- end
-end
-
-shared_context "converge failed" do
- let(:converge_error) do
- err = Chef::Exceptions::UnsupportedAction.new("Action unsupported")
- err.set_backtrace([ "/path/recipe.rb:15", "/path/recipe.rb:12" ])
- err
- end
-
- def stub_for_converge
- expect(Chef::Runner).to receive(:new).and_return(runner)
- expect(runner).to receive(:converge).and_raise(converge_error)
- end
-
- def stub_for_node_save
- expect(client).to_not receive(:save_updated_node)
- end
-end
-
-shared_context "audit phase completed" do
- def stub_for_audit
- # -- Client#run_audits
- expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner)
- expect(audit_runner).to receive(:run).and_return(true)
- expect(client.events).to receive(:audit_phase_complete)
- end
-end
-
-shared_context "audit phase failed with error" do
- let(:audit_error) do
- err = RuntimeError.new("Unexpected audit error")
- err.set_backtrace([ "/path/recipe.rb:57", "/path/recipe.rb:55" ])
- err
- end
-
- def stub_for_audit
- expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner)
- expect(Chef::Audit::Logger).to receive(:read_buffer).and_return("Audit mode output!")
- expect(audit_runner).to receive(:run).and_raise(audit_error)
- expect(client.events).to receive(:audit_phase_failed).with(audit_error, "Audit mode output!")
- end
-end
-
-shared_context "audit phase completed with failed controls" do
- let(:audit_runner) do
- instance_double("Chef::Audit::Runner", failed?: true,
- num_failed: 1, num_total: 3) end
-
- let(:audit_error) do
- err = Chef::Exceptions::AuditsFailed.new(audit_runner.num_failed, audit_runner.num_total)
- err.set_backtrace([ "/path/recipe.rb:108", "/path/recipe.rb:103" ])
- err
- end
-
- def stub_for_audit
- expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner)
- expect(Chef::Audit::Logger).to receive(:read_buffer).and_return("Audit mode output!")
- expect(audit_runner).to receive(:run)
- expect(Chef::Exceptions::AuditsFailed).to receive(:new).with(
- audit_runner.num_failed, audit_runner.num_total
- ).and_return(audit_error)
- expect(client.events).to receive(:audit_phase_failed).with(audit_error, "Audit mode output!")
- end
-end
-
-shared_context "run completed" do
- def stub_for_run
- expect(client).to receive(:run_completed_successfully)
-
- # --ResourceReporter#run_completed
- # updates the server with the resource history
- # (has its own tests, so stubbing it here.)
- expect_any_instance_of(Chef::ResourceReporter).to receive(:run_completed)
- # --AuditReporter#run_completed
- # posts the audit data to server.
- # (has its own tests, so stubbing it here.)
- expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_completed)
- end
-end
-
-shared_context "run failed" do
- def stub_for_run
- expect(client).to receive(:run_failed)
-
- # --ResourceReporter#run_completed
- # updates the server with the resource history
- # (has its own tests, so stubbing it here.)
- expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed)
- # --AuditReporter#run_completed
- # posts the audit data to server.
- # (has its own tests, so stubbing it here.)
- expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed)
- end
-
- before do
- expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError)
- end
-end
diff --git a/spec/support/shared/examples/client.rb b/spec/support/shared/examples/client.rb
deleted file mode 100644
index 6479c9d582..0000000000
--- a/spec/support/shared/examples/client.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-
-require "spec_helper"
-require "spec/support/shared/context/client"
-
-# requires platform and platform_version be defined
-shared_examples "a completed run" do
- include_context "run completed"
-
- it "runs ohai, sets up authentication, loads node state, synchronizes policy, converges, and runs audits" do
- # This is what we're testing.
- expect(client.run).to be true
-
- # fork is stubbed, so we can see the outcome of the run
- expect(node.automatic_attrs[:platform]).to eq(platform)
- expect(node.automatic_attrs[:platform_version]).to eq(platform_version)
- end
-
- describe "setting node GUID" do
- let(:chef_guid_path) { "/tmp/chef_guid" }
- let(:chef_guid) { "test-test-test" }
- let(:metadata_file) { "data_collector_metadata.json" }
- let(:metadata_path) { Pathname.new(File.join(Chef::Config[:file_cache_path], metadata_file)).cleanpath.to_s }
- let(:file) { instance_double(File) }
-
- before do
- allow(File).to receive(:read).and_call_original
- Chef::Config[:chef_guid_path] = chef_guid_path
- Chef::Config[:chef_guid] = nil
- end
-
- it "loads from the config" do
- expect(File).to receive(:exists?).with(chef_guid_path).and_return(true)
- expect(File).to receive(:read).with(chef_guid_path).and_return(chef_guid)
- client.run
- expect(Chef::Config[:chef_guid]).to eql(chef_guid)
- expect(node.automatic_attrs[:chef_guid]).to eql(chef_guid)
- end
-
- it "loads from the data collector config" do
- expect(File).to receive(:exists?).with(chef_guid_path).and_return(false)
- expect(Chef::FileCache).to receive(:load).with(metadata_file).and_return("{\"node_uuid\": \"#{chef_guid}\"}")
-
- expect(File).to receive(:open).with(chef_guid_path, "w+").and_yield(file)
- expect(file).to receive(:write).with(chef_guid)
-
- client.run
- expect(Chef::Config[:chef_guid]).to eql(chef_guid)
- expect(node.automatic_attrs[:chef_guid]).to eql(chef_guid)
- end
-
- it "creates a new one" do
- expect(File).to receive(:exists?).with(chef_guid_path).and_return(false)
- expect(File).to receive(:exists?).with(metadata_path).and_return(false)
-
- expect(SecureRandom).to receive(:uuid).and_return(chef_guid).at_least(:once)
-
- # we'll try and write the generated UUID to the data collector too, and that's ok
- allow(File).to receive(:open).with(metadata_path, "w", 420)
-
- expect(File).to receive(:open).with(chef_guid_path, "w+").and_yield(file)
- expect(file).to receive(:write).with(chef_guid)
-
- client.run
- expect(Chef::Config[:chef_guid]).to eql(chef_guid)
- expect(node.automatic_attrs[:chef_guid]).to eql(chef_guid)
- end
- end
-end
-
-shared_examples "a completed run with audit failure" do
- include_context "run completed"
-
- before do
- expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError)
- end
-
- it "converges, runs audits, saves the node and raises the error in a wrapping error" do
- expect { client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error|
- expect(error.wrapped_errors.size).to eq(run_errors.size)
- run_errors.each do |run_error|
- expect(error.wrapped_errors).to include(run_error)
- expect(error.backtrace).to include(*run_error.backtrace)
- end
- end
-
- # fork is stubbed, so we can see the outcome of the run
- expect(node.automatic_attrs[:platform]).to eq(platform)
- expect(node.automatic_attrs[:platform_version]).to eq(platform_version)
- end
-end
-
-shared_examples "a failed run" do
- include_context "run failed"
-
- it "skips node save and raises the error in a wrapping error" do
- expect { client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error|
- expect(error.wrapped_errors.size).to eq(run_errors.size)
- run_errors.each do |run_error|
- expect(error.wrapped_errors).to include(run_error)
- expect(error.backtrace).to include(*run_error.backtrace)
- end
- end
- end
-end
diff --git a/spec/unit/action_collection_spec.rb b/spec/unit/action_collection_spec.rb
new file mode 100644
index 0000000000..def3d5d7b4
--- /dev/null
+++ b/spec/unit/action_collection_spec.rb
@@ -0,0 +1,19 @@
+#
+# Copyright:: Copyright 2019-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.
+#
+
+# all the testing done on the action_collection presently is done in the
+# resource reporter and data collector tests
diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb
index 54bd7eab55..370d5d34e7 100644
--- a/spec/unit/client_spec.rb
+++ b/spec/unit/client_spec.rb
@@ -2,7 +2,7 @@
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Tim Hinderliter (<tim@chef.io>)
# Author:: Christopher Walters (<cw@chef.io>)
-# Copyright:: Copyright 2008-2017, Chef Software Inc.
+# Copyright:: Copyright 2008-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,9 +19,6 @@
#
require "spec_helper"
-require "spec/support/shared/context/client"
-require "spec/support/shared/examples/client"
-
require "chef/run_context"
require "chef/server_api"
require "rbconfig"
@@ -29,6 +26,344 @@ require "rbconfig"
class FooError < RuntimeError
end
+#
+# SHARED CONTEXTS
+#
+
+# Stubs a basic client object
+shared_context "client" do
+ let(:fqdn) { "hostname.example.org" }
+ let(:hostname) { "hostname" }
+ let(:machinename) { "machinename.example.org" }
+ let(:platform) { "example-platform" }
+ let(:platform_version) { "example-platform-1.0" }
+
+ let(:ohai_data) do
+ {
+ fqdn: fqdn,
+ hostname: hostname,
+ machinename: machinename,
+ platform: platform,
+ platform_version: platform_version,
+ }
+ end
+
+ let(:ohai_system) do
+ ohai = instance_double("Ohai::System", all_plugins: true, data: ohai_data, logger: logger)
+ allow(ohai).to receive(:[]) do |k|
+ ohai_data[k]
+ end
+ ohai
+ end
+
+ let(:node) do
+ Chef::Node.new.tap do |n|
+ n.name(fqdn)
+ n.chef_environment("_default")
+ end
+ end
+
+ let(:json_attribs) { nil }
+ let(:client_opts) { {} }
+
+ let(:stdout) { STDOUT }
+ let(:stderr) { STDERR }
+
+ let(:client) do
+ Chef::Config[:event_loggers] = []
+ allow(Ohai::System).to receive(:new).and_return(ohai_system)
+ opts = client_opts.merge({ logger: logger })
+ Chef::Client.new(json_attribs, opts).tap do |c|
+ c.node = node
+ end
+ end
+
+ let(:logger) { instance_double("Mixlib::Log::Child", trace: nil, debug: nil, warn: nil, info: nil, error: nil, fatal: nil) }
+
+ before do
+ stub_const("Chef::Client::STDOUT_FD", stdout)
+ stub_const("Chef::Client::STDERR_FD", stderr)
+ allow(client).to receive(:logger).and_return(logger)
+ end
+end
+
+# Stubs a client for a client run.
+# Requires a client object be defined in the scope of this included context.
+# e.g.:
+# describe "some functionality" do
+# include_context "client"
+# include_context "a client run"
+# ...
+# end
+shared_context "a client run" do
+ let(:stdout) { StringIO.new }
+ let(:stderr) { StringIO.new }
+
+ let(:api_client_exists?) { false }
+ let(:enable_fork) { false }
+
+ let(:http_data_collector) { double("Chef::ServerAPI (data collector)") }
+ let(:http_cookbook_sync) { double("Chef::ServerAPI (cookbook sync)") }
+ let(:http_node_load) { double("Chef::ServerAPI (node)") }
+ let(:http_node_save) { double("Chef::ServerAPI (node save)") }
+ let(:reporting_rest_client) { double("Chef::ServerAPI (reporting client)") }
+
+ let(:runner) { instance_double("Chef::Runner") }
+ let(:audit_runner) { instance_double("Chef::Audit::Runner", failed?: false) }
+
+ def stub_for_register
+ # --Client.register
+ # Make sure Client#register thinks the client key doesn't
+ # exist, so it tries to register and create one.
+ allow(File).to receive(:exists?).and_call_original
+ expect(File).to receive(:exists?)
+ .with(Chef::Config[:client_key])
+ .exactly(:once)
+ .and_return(api_client_exists?)
+
+ unless api_client_exists?
+ # Client.register will register with the validation client name.
+ expect_any_instance_of(Chef::ApiClient::Registration).to receive(:run)
+ end
+ end
+
+ def stub_for_event_registration
+ expect(client.events).to receive(:register).with(instance_of(Chef::DataCollector::Reporter))
+ expect(client.events).to receive(:register).with(instance_of(Chef::ResourceReporter))
+ expect(client.events).to receive(:register).with(instance_of(Chef::ActionCollection))
+ expect(client.events).to receive(:register).with(instance_of(Chef::Audit::AuditReporter))
+ end
+
+ def stub_for_node_load
+ # Client.register will then turn around create another
+ # Chef::ServerAPI object, this time with the client key it got from the
+ # previous step.
+ expect(Chef::ServerAPI).to receive(:new)
+ .with(Chef::Config[:chef_server_url], client_name: fqdn,
+ signing_key_filename: Chef::Config[:client_key])
+ .exactly(:once)
+ .and_return(http_node_load)
+
+ # --Client#build_node
+ # looks up the node, which we will return, then later saves it.
+ expect(Chef::Node).to receive(:find_or_create).with(fqdn).and_return(node)
+ end
+
+ def stub_rest_clean
+ allow(client).to receive(:rest_clean).and_return(reporting_rest_client)
+ end
+
+ def stub_for_sync_cookbooks
+ # --Client#setup_run_context
+ # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync
+ #
+ expect_any_instance_of(Chef::CookbookSynchronizer).to receive(:sync_cookbooks)
+ expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_url], version_class: Chef::CookbookManifestVersions).and_return(http_cookbook_sync)
+ expect(http_cookbook_sync).to receive(:post)
+ .with("environments/_default/cookbook_versions", { run_list: [] })
+ .and_return({})
+ end
+
+ def stub_for_required_recipe
+ response = Net::HTTPNotFound.new("1.1", "404", "Not Found")
+ exception = Net::HTTPClientException.new('404 "Not Found"', response)
+ expect(http_node_load).to receive(:get).with("required_recipe").and_raise(exception)
+ end
+
+ def stub_for_converge
+ # define me
+ end
+
+ def stub_for_audit
+ # define me
+ end
+
+ def stub_for_node_save
+ # define me
+ end
+
+ def stub_for_run
+ # define me
+ end
+
+ before do
+ Chef::Config[:client_fork] = enable_fork
+ Chef::Config[:cache_path] = windows? ? 'C:\chef' : "/var/chef"
+ Chef::Config[:why_run] = false
+ Chef::Config[:audit_mode] = :enabled
+ Chef::Config[:chef_guid] = "default-guid"
+
+ stub_rest_clean
+ stub_for_register
+ stub_for_event_registration
+ stub_for_node_load
+ stub_for_sync_cookbooks
+ stub_for_required_recipe
+ stub_for_converge
+ stub_for_audit
+ stub_for_node_save
+
+ expect_any_instance_of(Chef::RunLock).to receive(:acquire)
+ expect_any_instance_of(Chef::RunLock).to receive(:save_pid)
+ expect_any_instance_of(Chef::RunLock).to receive(:release)
+
+ # Post conditions: check that node has been filled in correctly
+ expect(client).to receive(:run_started)
+
+ stub_for_run
+ end
+end
+
+shared_context "converge completed" do
+ def stub_for_converge
+ # --Client#converge
+ expect(Chef::Runner).to receive(:new).and_return(runner)
+ expect(runner).to receive(:converge).and_return(true)
+ end
+
+ def stub_for_node_save
+ allow(node).to receive(:data_for_save).and_return(node.for_json)
+
+ # --Client#save_updated_node
+ expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_url], client_name: fqdn,
+ signing_key_filename: Chef::Config[:client_key], validate_utf8: false).and_return(http_node_save)
+ expect(http_node_save).to receive(:put).with("nodes/#{fqdn}", node.for_json).and_return(true)
+ end
+end
+
+shared_context "converge failed" do
+ let(:converge_error) do
+ err = Chef::Exceptions::UnsupportedAction.new("Action unsupported")
+ err.set_backtrace([ "/path/recipe.rb:15", "/path/recipe.rb:12" ])
+ err
+ end
+
+ def stub_for_converge
+ expect(Chef::Runner).to receive(:new).and_return(runner)
+ expect(runner).to receive(:converge).and_raise(converge_error)
+ end
+
+ def stub_for_node_save
+ expect(client).to_not receive(:save_updated_node)
+ end
+end
+
+shared_context "audit phase completed" do
+ def stub_for_audit
+ # -- Client#run_audits
+ expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner)
+ expect(audit_runner).to receive(:run).and_return(true)
+ expect(client.events).to receive(:audit_phase_complete)
+ end
+end
+
+shared_context "audit phase failed with error" do
+ let(:audit_error) do
+ err = RuntimeError.new("Unexpected audit error")
+ err.set_backtrace([ "/path/recipe.rb:57", "/path/recipe.rb:55" ])
+ err
+ end
+
+ def stub_for_audit
+ expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner)
+ expect(Chef::Audit::Logger).to receive(:read_buffer).and_return("Audit mode output!")
+ expect(audit_runner).to receive(:run).and_raise(audit_error)
+ expect(client.events).to receive(:audit_phase_failed).with(audit_error, "Audit mode output!")
+ end
+end
+
+shared_context "audit phase completed with failed controls" do
+ let(:audit_runner) do
+ instance_double("Chef::Audit::Runner", failed?: true,
+ num_failed: 1, num_total: 3) end
+
+ let(:audit_error) do
+ err = Chef::Exceptions::AuditsFailed.new(audit_runner.num_failed, audit_runner.num_total)
+ err.set_backtrace([ "/path/recipe.rb:108", "/path/recipe.rb:103" ])
+ err
+ end
+
+ def stub_for_audit
+ expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner)
+ expect(Chef::Audit::Logger).to receive(:read_buffer).and_return("Audit mode output!")
+ expect(audit_runner).to receive(:run)
+ expect(Chef::Exceptions::AuditsFailed).to receive(:new).with(
+ audit_runner.num_failed, audit_runner.num_total
+ ).and_return(audit_error)
+ expect(client.events).to receive(:audit_phase_failed).with(audit_error, "Audit mode output!")
+ end
+end
+
+shared_context "run completed" do
+ def stub_for_run
+ expect(client).to receive(:run_completed_successfully)
+ end
+end
+
+shared_context "run failed" do
+ def stub_for_run
+ expect(client).to receive(:run_failed)
+ end
+
+ before do
+ expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError)
+ end
+end
+
+#
+# SHARED EXAMPLES
+#
+
+# requires platform and platform_version be defined
+shared_examples "a completed run" do
+ include_context "run completed"
+
+ it "runs ohai, sets up authentication, loads node state, synchronizes policy, converges, and runs audits" do
+ # This is what we're testing.
+ expect(client.run).to be true
+
+ # fork is stubbed, so we can see the outcome of the run
+ expect(node.automatic_attrs[:platform]).to eq(platform)
+ expect(node.automatic_attrs[:platform_version]).to eq(platform_version)
+ end
+end
+
+shared_examples "a completed run with audit failure" do
+ include_context "run completed"
+
+ before do
+ expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError)
+ end
+
+ it "converges, runs audits, saves the node and raises the error in a wrapping error" do
+ expect { client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error|
+ expect(error.wrapped_errors.size).to eq(run_errors.size)
+ run_errors.each do |run_error|
+ expect(error.wrapped_errors).to include(run_error)
+ expect(error.backtrace).to include(*run_error.backtrace)
+ end
+ end
+
+ # fork is stubbed, so we can see the outcome of the run
+ expect(node.automatic_attrs[:platform]).to eq(platform)
+ expect(node.automatic_attrs[:platform_version]).to eq(platform_version)
+ end
+end
+
+shared_examples "a failed run" do
+ include_context "run failed"
+
+ it "skips node save and raises the error in a wrapping error" do
+ expect { client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error|
+ expect(error.wrapped_errors.size).to eq(run_errors.size)
+ run_errors.each do |run_error|
+ expect(error.wrapped_errors).to include(run_error)
+ expect(error.backtrace).to include(*run_error.backtrace)
+ end
+ end
+ end
+end
+
describe Chef::Client do
include_context "client"
diff --git a/spec/unit/data_collector/messages/helpers_spec.rb b/spec/unit/data_collector/messages/helpers_spec.rb
deleted file mode 100644
index a2c6753003..0000000000
--- a/spec/unit/data_collector/messages/helpers_spec.rb
+++ /dev/null
@@ -1,202 +0,0 @@
-#
-# Author:: Adam Leff (<adamleff@chef.io)
-#
-# Copyright:: Copyright 2012-2016, 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 "spec_helper"
-require "chef/data_collector/messages/helpers"
-
-class TestMessage
- extend Chef::DataCollector::Messages::Helpers
-end
-
-describe Chef::DataCollector::Messages::Helpers do
- describe "#organization" do
- context "when the run is a solo run" do
- it "returns the data collector organization" do
- allow(TestMessage).to receive(:solo_run?).and_return(true)
- expect(TestMessage).to receive(:data_collector_organization).and_return("org1")
- expect(TestMessage.organization).to eq("org1")
- end
- end
-
- context "when the run is not a solo run" do
- it "returns the data collector organization" do
- allow(TestMessage).to receive(:solo_run?).and_return(false)
- expect(TestMessage).to receive(:chef_server_organization).and_return("org2")
- expect(TestMessage.organization).to eq("org2")
- end
- end
- end
-
- describe "#data_collector_organization" do
- context "when the org is specified in the config" do
- it "returns the org from the config" do
- Chef::Config[:data_collector][:organization] = "org1"
- expect(TestMessage.data_collector_organization).to eq("org1")
- end
- end
-
- context "when the org is not specified in the config" do
- it "returns the default chef_solo org" do
- expect(TestMessage.data_collector_organization).to eq("chef_solo")
- end
- end
- end
-
- describe "#chef_server_organization" do
- context "when the URL is properly formatted" do
- it "returns the org from the parsed URL" do
- Chef::Config[:chef_server_url] = "http://mycompany.com/organizations/myorg"
- expect(TestMessage.chef_server_organization).to eq("myorg")
- end
- end
-
- context "when the URL is not properly formatted" do
- it "returns unknown_organization" do
- Chef::Config[:chef_server_url] = "http://mycompany.com/what/url/is/this"
- expect(TestMessage.chef_server_organization).to eq("unknown_organization")
- end
- end
-
- context "when the organization in the URL contains hyphens" do
- it "returns the full org name" do
- Chef::Config[:chef_server_url] = "http://mycompany.com/organizations/myorg-test"
- expect(TestMessage.chef_server_organization).to eq("myorg-test")
- end
- end
- end
-
- describe "#collector_source" do
- context "when the run is a solo run" do
- it "returns chef_solo" do
- allow(TestMessage).to receive(:solo_run?).and_return(true)
- expect(TestMessage.collector_source).to eq("chef_solo")
- end
- end
-
- context "when the run is not a solo run" do
- it "returns chef_client" do
- allow(TestMessage).to receive(:solo_run?).and_return(false)
- expect(TestMessage.collector_source).to eq("chef_client")
- end
- end
- end
-
- describe "#solo_run?" do
- context "when :solo is set in Chef::Config" do
- it "returns true" do
- Chef::Config[:solo] = true
- Chef::Config[:local_mode] = nil
- expect(TestMessage.solo_run?).to be_truthy
- end
- end
-
- context "when :local_mode is set in Chef::Config" do
- it "returns true" do
- Chef::Config[:solo] = nil
- Chef::Config[:local_mode] = true
- expect(TestMessage.solo_run?).to be_truthy
- end
- end
-
- context "when neither :solo or :local_mode is set in Chef::Config" do
- it "returns false" do
- Chef::Config[:solo] = nil
- Chef::Config[:local_mode] = nil
- expect(TestMessage.solo_run?).to be_falsey
- end
- end
- end
-
- describe "#node_uuid" do
- context "when the node UUID is available in Chef::Config" do
- it "returns the configured value" do
- Chef::Config[:chef_guid] = "configured_uuid"
- expect(TestMessage.node_uuid).to eq("configured_uuid")
- end
- end
-
- context "when the node UUID can be read" do
- it "returns the read-in node UUID" do
- Chef::Config[:chef_guid] = nil
- allow(TestMessage).to receive(:read_node_uuid).and_return("read_uuid")
- expect(TestMessage.node_uuid).to eq("read_uuid")
- end
- end
-
- context "when the node UUID cannot be read" do
- it "generated a new node UUID" do
- Chef::Config[:chef_guid] = nil
- allow(TestMessage).to receive(:read_node_uuid).and_return(nil)
- allow(TestMessage).to receive(:generate_node_uuid).and_return("generated_uuid")
- expect(TestMessage.node_uuid).to eq("generated_uuid")
- end
- end
- end
-
- describe "#generate_node_uuid" do
- it "generates a new UUID, stores it, and returns it" do
- expect(SecureRandom).to receive(:uuid).and_return("generated_uuid")
- expect(TestMessage).to receive(:update_metadata).with("node_uuid", "generated_uuid")
- expect(TestMessage.generate_node_uuid).to eq("generated_uuid")
- end
- end
-
- describe "#read_node_uuid" do
- it "reads the node UUID from metadata" do
- expect(TestMessage).to receive(:metadata).and_return({ "node_uuid" => "read_uuid" })
- expect(TestMessage.read_node_uuid).to eq("read_uuid")
- end
- end
-
- describe "metadata" do
- let(:metadata_filename) { "fake_metadata_file.json" }
-
- before do
- allow(TestMessage).to receive(:metadata_filename).and_return(metadata_filename)
- end
-
- context "when the metadata file exists" do
- it "returns the contents of the metadata file" do
- expect(Chef::FileCache).to receive(:load).with(metadata_filename).and_return('{"foo":"bar"}')
- expect(TestMessage.metadata["foo"]).to eq("bar")
- end
- end
-
- context "when the metadata file does not exist" do
- it "returns an empty hash" do
- expect(Chef::FileCache).to receive(:load).with(metadata_filename).and_raise(Chef::Exceptions::FileNotFound)
- expect(TestMessage.metadata).to eq({})
- end
- end
- end
-
- describe "#update_metadata" do
- it "updates the file" do
- allow(TestMessage).to receive(:metadata_filename).and_return("fake_metadata_file.json")
- allow(TestMessage).to receive(:metadata).and_return({ "key" => "current_value" })
- expect(Chef::FileCache).to receive(:store).with(
- "fake_metadata_file.json",
- '{"key":"updated_value"}',
- 0644
- )
-
- TestMessage.update_metadata("key", "updated_value")
- end
- end
-end
diff --git a/spec/unit/data_collector/messages_spec.rb b/spec/unit/data_collector/messages_spec.rb
deleted file mode 100644
index f5df85a988..0000000000
--- a/spec/unit/data_collector/messages_spec.rb
+++ /dev/null
@@ -1,329 +0,0 @@
-#
-# Author:: Adam Leff (<adamleff@chef.io)
-#
-# Copyright:: Copyright 2012-2016, 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 "spec_helper"
-require "ffi_yajl"
-require "chef/data_collector/messages/helpers"
-
-describe Chef::DataCollector::Messages do
- describe "#run_start_message" do
- let(:run_status) { Chef::RunStatus.new(Chef::Node.new, Chef::EventDispatch::Dispatcher.new) }
- let(:required_fields) do
- %w{
- chef_server_fqdn
- entity_uuid
- id
- message_version
- message_type
- node_name
- organization_name
- run_id
- source
- start_time
- }
- end
- let(:optional_fields) { [] }
-
- before do
- allow(run_status).to receive(:start_time).and_return(Time.now)
- end
-
- it "is not missing any required fields" do
- missing_fields = required_fields.select do |key|
- !Chef::DataCollector::Messages.run_start_message(run_status).key?(key)
- end
-
- expect(missing_fields).to eq([])
- end
-
- it "does not have any extra fields" do
- extra_fields = Chef::DataCollector::Messages.run_start_message(run_status).keys.select do |key|
- !required_fields.include?(key) && !optional_fields.include?(key)
- end
-
- expect(extra_fields).to eq([])
- end
- end
-
- describe "#run_end_message" do
- let(:node) { Chef::Node.new }
- let(:run_status) { Chef::RunStatus.new(node, Chef::EventDispatch::Dispatcher.new) }
- let(:report1) { double("report1", report_data: { "status" => "updated" }) }
- let(:report2) { double("report2", report_data: { "status" => "skipped" }) }
- let(:reporter_data) do
- {
- run_status: run_status,
- resources: [report1, report2],
- }
- end
-
- before do
- allow(run_status).to receive(:start_time).and_return(Time.now)
- allow(run_status).to receive(:end_time).and_return(Time.now)
- end
-
- it "includes a valid node object in the payload" do
- message = Chef::DataCollector::Messages.run_end_message(reporter_data)
- expect(message["node"]).to be_an_instance_of(Chef::Node)
- end
-
- it "returns a sane JSON representation of the node object" do
- node.chef_environment = "my_test_environment"
- node.run_list.add("recipe[my_test_cookbook::default]")
- message = FFI_Yajl::Parser.parse(Chef::DataCollector::Messages.run_end_message(reporter_data).to_json)
-
- expect(message["node"]["chef_environment"]).to eq("my_test_environment")
- expect(message["node"]["run_list"]).to eq(["recipe[my_test_cookbook::default]"])
- end
-
- context "when the run was successful" do
- let(:required_fields) do
- %w{
- chef_server_fqdn
- entity_uuid
- id
- end_time
- expanded_run_list
- message_type
- message_version
- node
- node_name
- organization_name
- resources
- run_id
- run_list
- source
- start_time
- status
- total_resource_count
- updated_resource_count
- deprecations
- }
- end
- let(:optional_fields) { %w{error policy_group policy_name} }
-
- before do
- allow(run_status).to receive(:exception).and_return(nil)
- end
-
- it "is not missing any required fields" do
- missing_fields = required_fields.select do |key|
- !Chef::DataCollector::Messages.run_end_message(reporter_data).key?(key)
- end
- expect(missing_fields).to eq([])
- end
-
- it "does not have any extra fields" do
- extra_fields = Chef::DataCollector::Messages.run_end_message(reporter_data).keys.select do |key|
- !required_fields.include?(key) && !optional_fields.include?(key)
- end
- expect(extra_fields).to eq([])
- end
-
- it "only includes updated resources in its count" do
- message = Chef::DataCollector::Messages.run_end_message(reporter_data)
- expect(message["total_resource_count"]).to eq(2)
- expect(message["updated_resource_count"]).to eq(1)
- end
- end
-
- context "when the run was not successful" do
- let(:required_fields) do
- %w{
- chef_server_fqdn
- entity_uuid
- id
- end_time
- error
- expanded_run_list
- message_type
- message_version
- node
- node_name
- organization_name
- resources
- run_id
- run_list
- source
- start_time
- status
- total_resource_count
- updated_resource_count
- deprecations
- }
- end
- let(:optional_fields) { %w{policy_group policy_name} }
-
- before do
- allow(run_status).to receive(:exception).and_return(RuntimeError.new("an error happened"))
- end
-
- it "is not missing any required fields" do
- missing_fields = required_fields.select do |key|
- !Chef::DataCollector::Messages.run_end_message(reporter_data).key?(key)
- end
- expect(missing_fields).to eq([])
- end
-
- it "does not have any extra fields" do
- extra_fields = Chef::DataCollector::Messages.run_end_message(reporter_data).keys.select do |key|
- !required_fields.include?(key) && !optional_fields.include?(key)
- end
- expect(extra_fields).to eq([])
- end
- end
- end
-
- describe "#run_end_message in policy mode" do
- let(:node) { Chef::Node.new }
- let(:run_status) { Chef::RunStatus.new(node, Chef::EventDispatch::Dispatcher.new) }
- let(:report1) { double("report1", report_data: { "status" => "updated" }) }
- let(:report2) { double("report2", report_data: { "status" => "skipped" }) }
- let(:reporter_data) do
- {
- run_status: run_status,
- resources: [report1, report2],
- }
- end
-
- before do
- allow(run_status).to receive(:start_time).and_return(Time.now)
- allow(run_status).to receive(:end_time).and_return(Time.now)
- node.policy_group = "test"
- node.policy_name = "policy-test"
- end
-
- it "includes a valid node object in the payload" do
- message = Chef::DataCollector::Messages.run_end_message(reporter_data)
- expect(message["node"]).to be_an_instance_of(Chef::Node)
- end
-
- it "returns a sane JSON representation of the node object" do
- node.chef_environment = "my_test_environment"
- node.run_list.add("recipe[my_test_cookbook::default]")
- message = FFI_Yajl::Parser.parse(Chef::DataCollector::Messages.run_end_message(reporter_data).to_json)
-
- expect(message["node"]["chef_environment"]).to eq("my_test_environment")
- expect(message["node"]["run_list"]).to eq(["recipe[my_test_cookbook::default]"])
- expect(message["node"]["policy_name"]).to eq("policy-test")
- expect(message["node"]["policy_group"]).to eq("test")
- end
-
- context "when the run was successful" do
- let(:required_fields) do
- %w{
- chef_server_fqdn
- entity_uuid
- id
- end_time
- expanded_run_list
- message_type
- message_version
- node
- node_name
- organization_name
- resources
- run_id
- run_list
- source
- start_time
- status
- total_resource_count
- updated_resource_count
- deprecations
- policy_name
- policy_group
- }
- end
- let(:optional_fields) { %w{error} }
-
- before do
- allow(run_status).to receive(:exception).and_return(nil)
- end
-
- it "is not missing any required fields" do
- missing_fields = required_fields.select do |key|
- !Chef::DataCollector::Messages.run_end_message(reporter_data).key?(key)
- end
- expect(missing_fields).to eq([])
- end
-
- it "does not have any extra fields" do
- extra_fields = Chef::DataCollector::Messages.run_end_message(reporter_data).keys.select do |key|
- !required_fields.include?(key) && !optional_fields.include?(key)
- end
- expect(extra_fields).to eq([])
- end
-
- it "only includes updated resources in its count" do
- message = Chef::DataCollector::Messages.run_end_message(reporter_data)
- expect(message["total_resource_count"]).to eq(2)
- expect(message["updated_resource_count"]).to eq(1)
- end
- end
-
- context "when the run was not successful" do
- let(:required_fields) do
- %w{
- chef_server_fqdn
- entity_uuid
- id
- end_time
- error
- expanded_run_list
- message_type
- message_version
- node
- node_name
- organization_name
- resources
- run_id
- run_list
- source
- start_time
- status
- total_resource_count
- updated_resource_count
- deprecations
- policy_name
- policy_group
- }
- end
- let(:optional_fields) { [] }
-
- before do
- allow(run_status).to receive(:exception).and_return(RuntimeError.new("an error happened"))
- end
-
- it "is not missing any required fields" do
- missing_fields = required_fields.select do |key|
- !Chef::DataCollector::Messages.run_end_message(reporter_data).key?(key)
- end
- expect(missing_fields).to eq([])
- end
-
- it "does not have any extra fields" do
- extra_fields = Chef::DataCollector::Messages.run_end_message(reporter_data).keys.select do |key|
- !required_fields.include?(key) && !optional_fields.include?(key)
- end
- expect(extra_fields).to eq([])
- end
- end
- end
-end
diff --git a/spec/unit/data_collector/resource_report_spec.rb b/spec/unit/data_collector/resource_report_spec.rb
deleted file mode 100644
index 1a5eab796e..0000000000
--- a/spec/unit/data_collector/resource_report_spec.rb
+++ /dev/null
@@ -1,145 +0,0 @@
-#
-# Author:: Salim Afiune (<afiune@chef.io)
-#
-# Copyright:: Copyright 2012-2018, 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 "spec_helper"
-
-describe Chef::DataCollector::ResourceReport do
- let(:cookbook_repo_path) { File.join(CHEF_SPEC_DATA, "cookbooks") }
- let(:cookbook_collection) { Chef::CookbookCollection.new(Chef::CookbookLoader.new(cookbook_repo_path)) }
- let(:node) { Chef::Node.new }
- let(:events) { Chef::EventDispatch::Dispatcher.new }
- let(:run_context) { Chef::RunContext.new(node, cookbook_collection, events) }
- let(:resource) { Chef::Resource.new("zelda", run_context) }
- let(:report) { described_class.new(resource, :create) }
-
- describe "#skipped" do
- let(:conditional) { double("Chef::Resource::Conditional") }
-
- it "should set status and conditional" do
- report.skipped(conditional)
- expect(report.conditional).to eq conditional
- expect(report.status).to eq "skipped"
- end
- end
-
- describe "#up_to_date" do
- it "should set status" do
- report.up_to_date
- expect(report.status).to eq "up-to-date"
- end
- end
-
- describe "#updated" do
- it "should set status" do
- report.updated
- expect(report.status).to eq "updated"
- end
- end
-
- describe "#elapsed_time_in_milliseconds" do
-
- context "when elapsed_time is not set" do
- it "should return nil" do
- allow(report).to receive(:elapsed_time).and_return(nil)
- expect(report.elapsed_time_in_milliseconds).to eq nil
- end
- end
-
- context "when elapsed_time is set" do
- it "should return it in milliseconds" do
- allow(report).to receive(:elapsed_time).and_return(1)
- expect(report.elapsed_time_in_milliseconds).to eq 1000
- end
- end
- end
-
- describe "#failed" do
- let(:exception) { double("Chef::Exception::Test") }
-
- it "should set exception and status" do
- report.failed(exception)
- expect(report.exception).to eq exception
- expect(report.status).to eq "failed"
- end
- end
-
- describe "#to_hash" do
- context "for a simple_resource" do
- let(:resource) do
- klass = Class.new(Chef::Resource) do
- resource_name "zelda"
- end
- klass.new("hyrule", run_context)
- end
- let(:hash) do
- {
- "after" => {},
- "before" => {},
- "delta" => "",
- "duration" => "",
- "id" => "hyrule",
- "ignore_failure" => false,
- "name" => "hyrule",
- "result" => "create",
- "status" => "unprocessed",
- "type" => :zelda,
- }
- end
-
- it "returns a hash containing the expected values" do
- expect(report.to_hash).to eq hash
- end
- end
-
- context "for a lazy_resource that got skipped" do
- let(:resource) do
- klass = Class.new(Chef::Resource) do
- resource_name "butters"
- property :sword, String, name_property: true, identity: true
- end
- resource = klass.new("hyrule")
- resource.sword = Chef::DelayedEvaluator.new { nil }
- resource
- end
- let(:hash) do
- {
- "after" => {},
- "before" => {},
- "delta" => "",
- "duration" => "",
- "conditional" => "because",
- "id" => "unknown identity (due to Chef::Exceptions::ValidationFailed)",
- "ignore_failure" => false,
- "name" => "hyrule",
- "result" => "create",
- "status" => "skipped",
- "type" => :butters,
- }
- end
- let(:conditional) do
- double("Chef::Resource::Conditional", to_text: "because")
- end
-
- it "should handle any Exception and throw a helpful message by mocking the identity" do
- report.skipped(conditional)
- expect(report.to_hash).to eq hash
- end
- end
- end
-end
diff --git a/spec/unit/data_collector_spec.rb b/spec/unit/data_collector_spec.rb
index 87729d8652..154ab4681c 100644
--- a/spec/unit/data_collector_spec.rb
+++ b/spec/unit/data_collector_spec.rb
@@ -1,8 +1,5 @@
#
-# Author:: Adam Leff (<adamleff@chef.io)
-# Author:: Ryan Cragun (<ryan@chef.io>)
-#
-# Copyright:: Copyright 2012-2018, Chef Software Inc.
+# Copyright:: Copyright 2019-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,859 +15,860 @@
# limitations under the License.
#
-require "spec_helper"
+require File.expand_path("../../spec_helper", __FILE__)
require "chef/data_collector"
-require "chef/resource_builder"
+require "socket"
describe Chef::DataCollector do
+ before(:each) do
+ Chef::Config[:enable_reporting] = true
+ end
- describe ".register_reporter?" do
- context "when no data collector URL or output locations are configured" do
- it "returns false" do
- Chef::Config[:data_collector][:server_url] = nil
- Chef::Config[:data_collector][:output_locations] = nil
- expect(Chef::DataCollector.register_reporter?).to be_falsey
- end
- end
-
- context "when a data collector URL is configured" do
- before do
- Chef::Config[:data_collector][:server_url] = "http://data_collector"
- end
-
- context "when operating in why_run mode" do
- it "returns false" do
- Chef::Config[:why_run] = true
- expect(Chef::DataCollector.register_reporter?).to be_falsey
- end
- end
-
- context "when not operating in why_run mode" do
-
- before do
- Chef::Config[:why_run] = false
- Chef::Config[:data_collector][:token] = token
- end
-
- context "when a token is configured" do
-
- let(:token) { "supersecrettoken" }
+ let(:node) { Chef::Node.new }
- context "when report is enabled for current mode" do
- it "returns true" do
- allow(Chef::DataCollector).to receive(:reporter_enabled_for_current_mode?).and_return(true)
- expect(Chef::DataCollector.register_reporter?).to be_truthy
- end
- end
+ let(:rest_client) { double("Chef::ServerAPI (mock)") }
- context "when report is disabled for current mode" do
- it "returns false" do
- allow(Chef::DataCollector).to receive(:reporter_enabled_for_current_mode?).and_return(false)
- expect(Chef::DataCollector.register_reporter?).to be_falsey
- end
- end
+ let(:data_collector) { Chef::DataCollector::Reporter.new(events) }
- end
+ let(:new_resource) { Chef::Resource::File.new("/tmp/a-file.txt") }
- # `Chef::Config[:data_collector][:server_url]` defaults to a URL
- # relative to the `chef_server_url`, so we use configuration of the
- # token to infer whether a solo/local mode user intends for data
- # collection to be enabled.
- context "when a token is not configured" do
+ let(:current_resource) { Chef::Resource::File.new("/tmp/a-file.txt") }
- let(:token) { nil }
+ let(:events) { Chef::EventDispatch::Dispatcher.new }
- context "when report is enabled for current mode" do
+ let(:run_context) { Chef::RunContext.new(node, {}, events) }
- before do
- allow(Chef::DataCollector).to receive(:reporter_enabled_for_current_mode?).and_return(true)
- end
+ let(:run_status) { Chef::RunStatus.new(node, events) }
- context "when the current mode is solo" do
+ let(:start_time) { Time.new }
- before do
- Chef::Config[:solo] = true
- end
+ let(:end_time) { Time.new + 20 }
- it "returns true" do
- expect(Chef::DataCollector.register_reporter?).to be(true)
- end
+ let(:run_list) { node.run_list }
- end
+ let(:run_id) { run_status.run_id }
- context "when the current mode is local mode" do
+ let(:expansion) { Chef::RunList::RunListExpansion.new("_default", run_list.run_list_items) }
- before do
- Chef::Config[:local_mode] = true
- end
+ let(:cookbook_name) { "monkey" }
- it "returns false" do
- expect(Chef::DataCollector.register_reporter?).to be(true)
- end
- end
+ let(:recipe_name) { "atlas" }
- context "when the current mode is client mode" do
+ let(:node_name) { "spitfire" }
- before do
- Chef::Config[:local_mode] = false
- Chef::Config[:solo] = false
- end
+ let(:cookbook_version) { double("Cookbook::Version", version: "1.2.3") }
- it "returns true" do
- expect(Chef::DataCollector.register_reporter?).to be_truthy
- end
+ let(:resource_record) { [] }
- end
+ let(:exception) { nil }
- end
+ let(:action_collection) { Chef::ActionCollection.new(events) }
- context "when report is disabled for current mode" do
- it "returns false" do
- allow(Chef::DataCollector).to receive(:reporter_enabled_for_current_mode?).and_return(false)
- expect(Chef::DataCollector.register_reporter?).to be_falsey
- end
- end
+ let(:expected_node) { node }
- end
-
- end
- end
+ let(:expected_expansion) { expansion }
- context "when output_locations are configured" do
- before do
- Chef::Config[:data_collector][:output_locations] = ["http://data_collector", "/tmp/data_collector.json"]
- end
+ let(:expected_run_list) { run_list.for_json }
- context "when operating in why_run mode" do
- it "returns false" do
- Chef::Config[:why_run] = true
- expect(Chef::DataCollector.register_reporter?).to be_falsey
- end
- end
+ let(:node_uuid) { "779196c6-f94f-4501-9dae-af8081ab4d3a" }
- context "when not operating in why_run mode" do
+ let(:request_id) { "5db5d686-d18d-4234-a86a-28848c35dfc2" }
- before do
- Chef::Config[:why_run] = false
- Chef::Config[:data_collector][:token] = token
- end
+ before do
+ allow(Time).to receive(:now).and_return(start_time, end_time)
+ allow(Chef::HTTP::SimpleJSON).to receive(:new).and_return(rest_client)
+ allow(Chef::ServerAPI).to receive(:new).and_return(rest_client)
+ node.name(node_name) unless node.is_a?(Hash)
+ new_resource.cookbook_name = cookbook_name
+ new_resource.recipe_name = recipe_name
+ allow(new_resource).to receive(:cookbook_version).and_return(cookbook_version)
+
+ run_list << "recipe[lobster]" << "role[rage]" << "recipe[fist]"
+ events.register(data_collector)
+ events.register(action_collection)
+ run_status.run_id = request_id
+ events.run_start(Chef::VERSION, run_status)
+ Chef::Config[:chef_guid] = node_uuid
+ # we're guaranteed that those events are processed or else the data collector has no hope
+ # all other events could see the chef-client crash before executing them and the data collector
+ # still needs to work in those cases, so must come later, and the failure cases must be tested.
+ end
- context "when a token is configured" do
+ def expect_start_message(keys = nil)
+ keys ||= {
+ "chef_server_fqdn" => "localhost",
+ "entity_uuid" => node_uuid,
+ "id" => request_id,
+ "message_type" => "run_start",
+ "message_version" => "1.0.0",
+ "node_name" => node_name,
+ "organization_name" => "unknown_organization",
+ "run_id" => request_id,
+ "source" => "chef_client",
+ "start_time" => start_time.utc.iso8601,
+ }
+ expect(rest_client).to receive(:post).with(
+ nil,
+ hash_including(keys),
+ { "Content-Type" => "application/json" }
+ )
+ end
- let(:token) { "supersecrettoken" }
+ def expect_converge_message(keys)
+ keys["message_type"] = "run_converge"
+ keys["message_version"] = "1.1.0"
+ expect(rest_client).to receive(:post).with(
+ nil,
+ hash_including(keys),
+ { "Content-Type" => "application/json" }
+ )
+ end
- context "when report is enabled for current mode" do
- it "returns true" do
- allow(Chef::DataCollector).to receive(:reporter_enabled_for_current_mode?).and_return(true)
- expect(Chef::DataCollector.register_reporter?).to be_truthy
- end
- end
+ def resource_has_diff(new_resource, status)
+ new_resource.respond_to?(:diff) && %w{updated failed}.include?(status)
+ end
- context "when report is disabled for current mode" do
- it "returns false" do
- allow(Chef::DataCollector).to receive(:reporter_enabled_for_current_mode?).and_return(false)
- expect(Chef::DataCollector.register_reporter?).to be_falsey
- end
- end
+ def resource_record_for(current_resource, new_resource, action, status, duration)
+ {
+ "after" => new_resource.state_for_resource_reporter,
+ "before" => current_resource&.state_for_resource_reporter,
+ "cookbook_name" => cookbook_name,
+ "cookbook_version" => cookbook_version.version,
+ "delta" => resource_has_diff(new_resource, status) ? new_resource.diff : "",
+ "duration" => duration,
+ "id" => new_resource.identity,
+ "ignore_failure" => new_resource.ignore_failure,
+ "name" => new_resource.name,
+ "recipe_name" => recipe_name,
+ "result" => action.to_s,
+ "status" => status,
+ "type" => new_resource.resource_name.to_sym,
+ }
+ end
- end
+ def send_run_failed_or_completed_event
+ status == "success" ? events.run_completed(node, run_status) : events.run_failed(exception, run_status)
+ end
- # `Chef::Config[:data_collector][:server_url]` defaults to a URL
- # relative to the `chef_server_url`, so we use configuration of the
- # token to infer whether a solo/local mode user intends for data
- # collection to be enabled.
- context "when a token is not configured" do
+ shared_examples_for "sends a converge message" do
+ it "has a chef_server_fqdn" do
+ expect_converge_message("chef_server_fqdn" => "localhost")
+ send_run_failed_or_completed_event
+ end
- let(:token) { nil }
+ it "has a start_time" do
+ expect_converge_message("start_time" => start_time.utc.iso8601)
+ send_run_failed_or_completed_event
+ end
- context "when report is enabled for current mode" do
+ it "has a end_time" do
+ expect_converge_message("end_time" => end_time.utc.iso8601)
+ send_run_failed_or_completed_event
+ end
- before do
- allow(Chef::DataCollector).to receive(:reporter_enabled_for_current_mode?).and_return(true)
- end
+ it "has a entity_uuid" do
+ expect_converge_message("entity_uuid" => node_uuid)
+ send_run_failed_or_completed_event
+ end
- context "when the current mode is solo" do
+ it "has a expanded_run_list" do
+ expect_converge_message("expanded_run_list" => expected_expansion)
+ send_run_failed_or_completed_event
+ end
- before do
- Chef::Config[:solo] = true
- end
+ it "has a node" do
+ expect_converge_message("node" => expected_node)
+ send_run_failed_or_completed_event
+ end
- it "returns true" do
- expect(Chef::DataCollector.register_reporter?).to be(true)
- end
+ it "has a node_name" do
+ expect_converge_message("node_name" => node_name)
+ send_run_failed_or_completed_event
+ end
- end
+ it "has an organization" do
+ expect_converge_message("organization_name" => "unknown_organization")
+ send_run_failed_or_completed_event
+ end
- context "when the current mode is local mode" do
+ it "has a policy_group" do
+ expect_converge_message("policy_group" => nil)
+ send_run_failed_or_completed_event
+ end
- before do
- Chef::Config[:local_mode] = true
- end
+ it "has a policy_name" do
+ expect_converge_message("policy_name" => nil)
+ send_run_failed_or_completed_event
+ end
- it "returns false" do
- expect(Chef::DataCollector.register_reporter?).to be(true)
- end
- end
+ it "has a run_id" do
+ expect_converge_message("run_id" => request_id)
+ send_run_failed_or_completed_event
+ end
- context "when the current mode is client mode" do
+ it "has a run_list" do
+ expect_converge_message("run_list" => expected_run_list)
+ send_run_failed_or_completed_event
+ end
- before do
- Chef::Config[:local_mode] = false
- Chef::Config[:solo] = false
- end
+ it "has a source" do
+ expect_converge_message("source" => "chef_client")
+ send_run_failed_or_completed_event
+ end
- it "returns true" do
- expect(Chef::DataCollector.register_reporter?).to be_truthy
- end
+ it "has a status" do
+ expect_converge_message("status" => status)
+ send_run_failed_or_completed_event
+ end
- end
+ it "has no deprecations" do
+ expect_converge_message("deprecations" => [])
+ send_run_failed_or_completed_event
+ end
- end
+ it "has an error field" do
+ if exception
+ expect_converge_message(
+ "error" => {
+ "class" => exception.class,
+ "message" => exception.message,
+ "backtrace" => exception.backtrace,
+ "description" => error_description,
+ }
+ )
+ else
+ expect(rest_client).to receive(:post).with(
+ nil,
+ hash_excluding("error"),
+ { "Content-Type" => "application/json" }
+ )
+ end
+ send_run_failed_or_completed_event
+ end
- context "when report is disabled for current mode" do
- it "returns false" do
- allow(Chef::DataCollector).to receive(:reporter_enabled_for_current_mode?).and_return(false)
- expect(Chef::DataCollector.register_reporter?).to be_falsey
- end
- end
+ it "has a total resource count of zero" do
+ expect_converge_message("total_resource_count" => total_resource_count)
+ send_run_failed_or_completed_event
+ end
- end
+ it "has a updated resource count of zero" do
+ expect_converge_message("updated_resource_count" => updated_resource_count)
+ send_run_failed_or_completed_event
+ end
- end
+ it "includes the resource record" do
+ expect_converge_message("resources" => resource_record)
+ send_run_failed_or_completed_event
end
end
- describe ".reporter_enabled_for_current_mode?" do
- context "when running in solo mode" do
- before do
- Chef::Config[:solo] = true
- Chef::Config[:local_mode] = false
+ describe "#should_be_enabled?" do
+ shared_examples_for "a solo-like run" do
+ it "is disabled in solo-legacy without a data_collector url and token" do
+ expect(data_collector.send(:should_be_enabled?)).to be false
end
- context "when data_collector_mode is :solo" do
- it "returns true" do
- Chef::Config[:data_collector][:mode] = :solo
- expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(true)
- end
+ it "is disabled in solo-legacy with only a url" do
+ Chef::Config[:data_collector][:server_url] = "https://www.esa.local/ariane5"
+ expect(data_collector.send(:should_be_enabled?)).to be false
end
- context "when data_collector_mode is :client" do
- it "returns false" do
- Chef::Config[:data_collector][:mode] = :client
- expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(false)
- end
- end
-
- context "when data_collector_mode is :both" do
- it "returns true" do
- Chef::Config[:data_collector][:mode] = :both
- expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(true)
- end
+ it "is disabled in solo-legacy with only a token" do
+ Chef::Config[:data_collector][:token] = "admit_one"
+ expect(data_collector.send(:should_be_enabled?)).to be false
end
- end
- context "when running in local mode" do
- before do
- Chef::Config[:solo] = false
- Chef::Config[:local_mode] = true
+ it "is enabled in solo-legacy with both a token and url" do
+ Chef::Config[:data_collector][:server_url] = "https://www.esa.local/ariane5"
+ Chef::Config[:data_collector][:token] = "no_cash_value"
+ expect(data_collector.send(:should_be_enabled?)).to be true
end
- context "when data_collector_mode is :solo" do
- it "returns true" do
- Chef::Config[:data_collector][:mode] = :solo
- expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(true)
- end
+ it "is enabled in solo-legacy with only an output location to a file" do
+ Chef::Config[:data_collector][:output_locations] = { files: [ "/always/be/counting/down" ] }
+ expect(data_collector.send(:should_be_enabled?)).to be true
end
- context "when data_collector_mode is :client" do
- it "returns false" do
- Chef::Config[:data_collector][:mode] = :client
- expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(false)
- end
+ it "is disabled in solo-legacy with only an output location to a uri" do
+ Chef::Config[:data_collector][:output_locations] = { urls: [ "https://esa.local/ariane5" ] }
+ expect(data_collector.send(:should_be_enabled?)).to be false
end
- context "when data_collector_mode is :both" do
- it "returns true" do
- Chef::Config[:data_collector][:mode] = :both
- expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(true)
- end
+ it "is enabled in solo-legacy with only an output location to a uri with a token" do
+ Chef::Config[:data_collector][:output_locations] = { urls: [ "https://esa.local/ariane5" ] }
+ Chef::Config[:data_collector][:token] = "good_for_one_fare"
+ expect(data_collector.send(:should_be_enabled?)).to be true
end
- end
- context "when running in client mode" do
- before do
- Chef::Config[:solo] = false
- Chef::Config[:local_mode] = false
+ it "is enabled in solo-legacy when the mode is :solo" do
+ Chef::Config[:data_collector][:server_url] = "https://www.esa.local/ariane5"
+ Chef::Config[:data_collector][:token] = "non_redeemable"
+ Chef::Config[:data_collector][:mode] = :solo
+ expect(data_collector.send(:should_be_enabled?)).to be true
end
- context "when data_collector_mode is :solo" do
- it "returns false" do
- Chef::Config[:data_collector][:mode] = :solo
- expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(false)
- end
+ it "is enabled in solo-legacy when the mode is :both" do
+ Chef::Config[:data_collector][:server_url] = "https://www.esa.local/ariane5"
+ Chef::Config[:data_collector][:token] = "non_negotiable"
+ Chef::Config[:data_collector][:mode] = :both
+ expect(data_collector.send(:should_be_enabled?)).to be true
end
- context "when data_collector_mode is :client" do
- it "returns true" do
- Chef::Config[:data_collector][:mode] = :client
- expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(true)
- end
+ it "is disabled in solo-legacy when the mode is :client" do
+ Chef::Config[:data_collector][:server_url] = "https://www.esa.local/ariane5"
+ Chef::Config[:data_collector][:token] = "NYCTA"
+ Chef::Config[:data_collector][:mode] = :client
+ expect(data_collector.send(:should_be_enabled?)).to be false
end
- context "when data_collector_mode is :both" do
- it "returns true" do
- Chef::Config[:data_collector][:mode] = :both
- expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(true)
- end
+ it "is disabled in solo-legacy mode when the mode is :nonsense" do
+ Chef::Config[:data_collector][:server_url] = "https://www.esa.local/ariane5"
+ Chef::Config[:data_collector][:token] = "MTA"
+ Chef::Config[:data_collector][:mode] = :nonsense
+ expect(data_collector.send(:should_be_enabled?)).to be false
end
end
- end
-
-end
-
-describe Chef::DataCollector::Reporter do
- let(:reporter) { described_class.new }
- let(:run_status) { Chef::RunStatus.new(Chef::Node.new, Chef::EventDispatch::Dispatcher.new) }
-
- let(:token) { "supersecrettoken" }
-
- before do
- Chef::Config[:data_collector][:server_url] = "http://my-data-collector-server.mycompany.com"
- Chef::Config[:data_collector][:token] = token
- end
-
- describe "selecting token or signed header authentication" do
-
- context "when the token is set in the config" do
-
- before do
- Chef::Config[:client_key] = "/no/key/should/exist/at/this/path.pem"
- end
-
- it "configures an HTTP client that doesn't do signed header auth" do
- # Initializing with the wrong kind of HTTP class should cause Chef::Exceptions::PrivateKeyMissing
- expect { reporter.http }.to_not raise_error
- end
+ it "by default it is enabled" do
+ expect(data_collector.send(:should_be_enabled?)).to be true
end
- context "when no token is set in the config" do
-
- let(:token) { nil }
-
- let(:client_key) { File.join(CHEF_SPEC_DATA, "ssl", "private_key.pem") }
+ it "is disabled in why-run" do
+ Chef::Config[:why_run] = true
+ expect(data_collector.send(:should_be_enabled?)).to be false
+ end
- before do
- Chef::Config[:client_key] = client_key
+ describe "a solo legacy run" do
+ before(:each) do
+ Chef::Config[:solo_legacy_mode] = true
end
- it "configures an HTTP client that does signed header auth" do
- expect { reporter.http }.to_not raise_error
- expect(reporter.http.options).to have_key(:signing_key_filename)
- expect(reporter.http.options[:signing_key_filename]).to eq(client_key)
- end
+ it_behaves_like "a solo-like run"
end
- end
-
- describe "#run_started" do
- before do
- allow(reporter).to receive(:update_run_status)
- allow(reporter).to receive(:send_to_data_collector)
- allow(Chef::DataCollector::Messages).to receive(:run_start_message)
- end
+ describe "a local mode run" do
+ before(:each) do
+ Chef::Config[:local_mode] = true
+ end
- it "updates the run status" do
- expect(reporter).to receive(:update_run_status).with(run_status)
- reporter.run_started(run_status)
+ it_behaves_like "a solo-like run"
end
- it "sends the RunStart message output to the Data Collector server" do
- expect(Chef::DataCollector::Messages)
- .to receive(:run_start_message)
- .with(run_status)
- .and_return(key: "value")
- expect(reporter).to receive(:send_to_data_collector).with({ key: "value" })
- reporter.run_started(run_status)
+ it "is enabled in client mode when the mode is :both" do
+ Chef::Config[:data_collector][:mode] = :both
+ expect(data_collector.send(:should_be_enabled?)).to be true
end
- end
-
- describe "when sending a message at chef run completion" do
- let(:node) { Chef::Node.new }
-
- let(:run_status) do
- instance_double("Chef::RunStatus",
- run_id: "run_id",
- node: node,
- start_time: Time.new,
- end_time: Time.new,
- exception: exception)
+ it "is disabled in client mode when the mode is :solo" do
+ Chef::Config[:data_collector][:mode] = :solo
+ expect(data_collector.send(:should_be_enabled?)).to be false
end
- before do
- reporter.send(:update_run_status, run_status)
+ it "is disabled in client mode when the mode is :nonsense" do
+ Chef::Config[:data_collector][:mode] = :nonsense
+ expect(data_collector.send(:should_be_enabled?)).to be false
end
- describe "#run_completed" do
-
- let(:exception) { nil }
-
- it "sends the run completion" do
- expect(reporter).to receive(:send_to_data_collector) do |message|
- expect(message).to be_a(Hash)
- expect(message["status"]).to eq("success")
- end
- reporter.run_completed(node)
- end
- end
-
- describe "#run_failed" do
-
- let(:exception) { StandardError.new("oops") }
-
- it "updates the exception and sends the run completion" do
- expect(reporter).to receive(:send_to_data_collector) do |message|
- expect(message).to be_a(Hash)
- expect(message["status"]).to eq("failure")
- end
- reporter.run_failed("test_exception")
- end
+ it "is still enabled if you set a token in client mode" do
+ Chef::Config[:data_collector][:token] = "good_for_one_ride"
+ expect(data_collector.send(:should_be_enabled?)).to be true
end
end
- describe "#converge_start" do
- it "stashes the run_context for later use" do
- reporter.converge_start("test_context")
- expect(reporter.run_context).to eq("test_context")
- end
- end
+ describe "when the run fails during node load" do
+ let(:exception) { Exception.new("imperial to metric conversion error") }
+ let(:error_description) { Chef::Formatters::ErrorMapper.registration_failed(node_name, exception, Chef::Config).for_json }
+ let(:total_resource_count) { 0 }
+ let(:updated_resource_count) { 0 }
+ let(:status) { "failure" }
+ let(:expected_node) { {} } # no node because that failed
+ let(:expected_run_list) { [] } # no run_list without a node
+ let(:expected_expansion) { {} } # no run_list expansion without a run_list
+ let(:resource_record) { [] } # and certainly no resources
- describe "#converge_complete" do
- it "detects and processes any unprocessed resources" do
- expect(reporter).to receive(:detect_unprocessed_resources)
- reporter.converge_complete
+ before do
+ events.registration_failed(node_name, exception, Chef::Config)
+ run_status.stop_clock
+ run_status.exception = exception
+ expect_start_message
end
- end
- describe "#converge_failed" do
- it "detects and processes any unprocessed resources" do
- expect(reporter).to receive(:detect_unprocessed_resources)
- reporter.converge_failed("exception")
- end
+ it_behaves_like "sends a converge message"
end
- describe "#resource_current_state_loaded" do
- let(:new_resource) { double("new_resource") }
- let(:action) { double("action") }
- let(:current_resource) { double("current_resource") }
- let(:resource_report) { double("resource_report") }
+ describe "when the run fails during node load" do
+ let(:exception) { Exception.new("imperial to metric conversion error") }
+ let(:error_description) { Chef::Formatters::ErrorMapper.node_load_failed(node_name, exception, Chef::Config).for_json }
+ let(:total_resource_count) { 0 }
+ let(:updated_resource_count) { 0 }
+ let(:status) { "failure" }
+ let(:expected_node) { {} } # no node because that failed
+ let(:expected_run_list) { [] } # no run_list without a node
+ let(:expected_expansion) { {} } # no run_list expansion without a run_list
+ let(:resource_record) { [] } # and certainly no resources
- context "when resource is a nested resource" do
- it "does not update the resource report" do
- allow(reporter).to receive(:nested_resource?).and_return(true)
- expect(reporter).not_to receive(:update_current_resource_report)
- reporter.resource_current_state_loaded(new_resource, action, current_resource)
- end
+ before do
+ events.node_load_failed(node_name, exception, Chef::Config)
+ run_status.stop_clock
+ run_status.exception = exception
+ expect_start_message
end
- context "when resource is not a nested resource" do
- it "initializes the resource report" do
- allow(reporter).to receive(:nested_resource?).and_return(false)
- expect(reporter).to receive(:initialize_resource_report_if_needed)
- .with(new_resource, action, current_resource)
- reporter.resource_current_state_loaded(new_resource, action, current_resource)
- end
- end
+ it_behaves_like "sends a converge message"
end
- describe "#resource_up_to_date" do
- let(:new_resource) { double("new_resource") }
- let(:action) { double("action") }
- let(:resource_report) { double("resource_report") }
+ describe "when the run fails during run_list_expansion" do
+ let(:exception) { Exception.new("imperial to metric conversion error") }
+ let(:error_description) { Chef::Formatters::ErrorMapper.run_list_expand_failed(node, exception).for_json }
+ let(:total_resource_count) { 0 }
+ let(:updated_resource_count) { 0 }
+ let(:status) { "failure" }
+ let(:expected_expansion) { {} } # no run_list expanasion when it failed
+ let(:resource_record) { [] } # and no resources
before do
- allow(reporter).to receive(:nested_resource?)
- allow(reporter).to receive(:current_resource_report).and_return(resource_report)
- allow(resource_report).to receive(:up_to_date)
- end
-
- context "when the resource is a nested resource" do
- it "does not mark the resource report as up-to-date" do
- allow(reporter).to receive(:nested_resource?).with(new_resource).and_return(true)
- expect(resource_report).not_to receive(:up_to_date)
- reporter.resource_up_to_date(new_resource, action)
- end
+ events.node_load_success(node)
+ run_status.node = node
+ events.run_list_expand_failed(node, exception)
+ run_status.stop_clock
+ run_status.exception = exception
+ expect_start_message
end
- context "when the resource is not a nested resource" do
- it "marks the resource report as up-to-date" do
- allow(reporter).to receive(:nested_resource?).with(new_resource).and_return(false)
- expect(resource_report).to receive(:up_to_date)
- reporter.resource_up_to_date(new_resource, action)
- end
- end
+ it_behaves_like "sends a converge message"
end
- describe "#resource_skipped" do
- let(:new_resource) { double("new_resource") }
- let(:action) { double("action") }
- let(:conditional) { double("conditional") }
- let(:resource_report) { double("resource_report") }
+ describe "when the run fails during cookbook resolution" do
+ let(:exception) { Exception.new("imperial to metric conversion error") }
+ let(:error_description) { Chef::Formatters::ErrorMapper.cookbook_resolution_failed(node, exception).for_json }
+ let(:total_resource_count) { 0 }
+ let(:updated_resource_count) { 0 }
+ let(:status) { "failure" }
+ let(:resource_record) { [] } # and no resources
before do
- allow(reporter).to receive(:nested_resource?)
- allow(resource_report).to receive(:skipped)
+ events.node_load_success(node)
+ run_status.node = node
+ events.run_list_expanded(expansion)
+ run_status.start_clock
+ expect_start_message
+ events.cookbook_resolution_failed(node, exception)
+ run_status.stop_clock
+ run_status.exception = exception
end
- context "when the resource is a nested resource" do
- it "does not mark the resource report as skipped" do
- allow(reporter).to receive(:nested_resource?).with(new_resource).and_return(true)
- expect(resource_report).not_to receive(:skipped).with(conditional)
- reporter.resource_skipped(new_resource, action, conditional)
- end
- end
-
- context "when the resource is not a nested resource" do
- it "initializes the resource report and marks it as skipped" do
- allow(reporter).to receive(:nested_resource?).and_return(false)
- allow(reporter).to receive(:current_resource_report).and_return(resource_report)
- expect(reporter).to receive(:initialize_resource_report_if_needed).with(new_resource, action)
- expect(resource_report).to receive(:skipped).with(conditional)
- reporter.resource_skipped(new_resource, action, conditional)
- end
- end
+ it_behaves_like "sends a converge message"
end
- describe "#resource_updated" do
- let(:resource_report) { double("resource_report") }
+ describe "when the run fails during cookbook synchronization" do
+ let(:exception) { Exception.new("imperial to metric conversion error") }
+ let(:error_description) { Chef::Formatters::ErrorMapper.cookbook_sync_failed(node, exception).for_json }
+ let(:total_resource_count) { 0 }
+ let(:updated_resource_count) { 0 }
+ let(:status) { "failure" }
+ let(:resource_record) { [] } # and no resources
before do
- allow(reporter).to receive(:current_resource_report).and_return(resource_report)
- allow(resource_report).to receive(:updated)
+ events.node_load_success(node)
+ run_status.node = node
+ events.run_list_expanded(expansion)
+ run_status.start_clock
+ expect_start_message
+ events.cookbook_sync_failed(node, exception)
+ run_status.stop_clock
+ run_status.exception = exception
end
- it "marks the resource report as updated" do
- expect(resource_report).to receive(:updated)
- reporter.resource_updated("new_resource", "action")
- end
+ it_behaves_like "sends a converge message"
end
- describe "#resource_failed" do
- let(:new_resource) { double("new_resource") }
- let(:action) { double("action") }
- let(:exception) { double("exception") }
- let(:error_mapper) { double("error_mapper") }
- let(:resource_report) { double("resource_report") }
-
+ describe "after successfully starting the run" do
before do
- allow(reporter).to receive(:update_error_description)
- allow(reporter).to receive(:current_resource_report).and_return(resource_report)
- allow(resource_report).to receive(:failed)
- allow(Chef::Formatters::ErrorMapper).to receive(:resource_failed).and_return(error_mapper)
- allow(error_mapper).to receive(:for_json)
+ # these events happen in this order in the client
+ events.node_load_success(node)
+ run_status.node = node
+ events.run_list_expanded(expansion)
+ run_status.start_clock
end
- it "updates the error description" do
- expect(Chef::Formatters::ErrorMapper).to receive(:resource_failed).with(
- new_resource,
- action,
- exception
- ).and_return(error_mapper)
- expect(error_mapper).to receive(:for_json).and_return("error_description")
- expect(reporter).to receive(:update_error_description).with("error_description")
- reporter.resource_failed(new_resource, action, exception)
- end
+ describe "run_start_message" do
+ it "sends a run_start_message" do
+ expect_start_message
+ events.run_started(run_status)
+ end
- context "when the resource is not a nested resource" do
- it "marks the resource report as failed" do
- allow(reporter).to receive(:nested_resource?).with(new_resource).and_return(false)
- expect(resource_report).to receive(:failed).with(exception)
- reporter.resource_failed(new_resource, action, exception)
+ it "extracts the hostname from the chef_server_url" do
+ Chef::Config[:chef_server_url] = "https://spacex.rockets.local"
+ expect_start_message("chef_server_fqdn" => "spacex.rockets.local")
+ events.run_started(run_status)
end
- end
- context "when the resource is a nested resource" do
- it "does not mark the resource report as failed" do
- allow(reporter).to receive(:nested_resource?).with(new_resource).and_return(true)
- expect(resource_report).not_to receive(:failed).with(exception)
- reporter.resource_failed(new_resource, action, exception)
+ it "extracts the organization from the chef_server_url" do
+ Chef::Config[:chef_server_url] = "https://spacex.rockets.local/organizations/gnc"
+ expect_start_message("organization_name" => "gnc")
+ events.run_started(run_status)
end
- end
- end
- describe "#resource_completed" do
- let(:new_resource) { double("new_resource") }
- let(:resource_report) { double("resource_report") }
+ it "extracts the organization from the chef_server_url if there are extra slashes" do
+ Chef::Config[:chef_server_url] = "https://spacex.rockets.local///organizations///gnc"
+ expect_start_message("organization_name" => "gnc")
+ events.run_started(run_status)
+ end
- before do
- allow(reporter).to receive(:update_current_resource_report)
- allow(reporter).to receive(:add_resource_report)
- allow(reporter).to receive(:current_resource_report)
- allow(resource_report).to receive(:finish)
- end
+ it "extracts the organization from the chef_server_url if there is a trailing slash" do
+ Chef::Config[:chef_server_url] = "https://spacex.rockets.local/organizations/gnc/"
+ expect_start_message("organization_name" => "gnc")
+ events.run_started(run_status)
+ end
- context "when there is no current resource report" do
- it "does not touch the current resource report" do
- allow(reporter).to receive(:current_resource_report).and_return(nil)
- expect(reporter).not_to receive(:update_current_resource_report)
- reporter.resource_completed(new_resource)
+ it "sets 'unknown_organization' if the cher_server_url does not contain one" do
+ Chef::Config[:chef_server_url] = "https://spacex.rockets.local"
+ expect_start_message("organization_name" => "unknown_organization")
+ events.run_started(run_status)
end
- end
- context "when there is a current resource report" do
- before do
- allow(reporter).to receive(:current_resource_report).and_return(resource_report)
+ it "still uses the chef_server_url in non-solo mode even if the data_collector organization is set" do
+ Chef::Config[:data_collector][:organization] = "blue-origin"
+ Chef::Config[:chef_server_url] = "https://spacex.rockets.local/organizations/gnc/"
+ expect_start_message("organization_name" => "gnc")
+ events.run_started(run_status)
end
- context "when the resource is a nested resource" do
- it "does not mark the resource as finished" do
- allow(reporter).to receive(:nested_resource?).with(new_resource).and_return(true)
- expect(resource_report).not_to receive(:finish)
- reporter.resource_completed(new_resource)
+ describe "in legacy mode" do
+ before do
+ Chef::Config[:solo_legacy_mode] = true
+ Chef::Config[:data_collector][:server_url] = "https://nasa.rockets.local/organizations/sls"
+ end
+
+ it "we get the data collector organization" do
+ Chef::Config[:data_collector][:organization] = "blue-origin"
+ Chef::Config[:chef_server_url] = "https://spacex.rockets.local/organizations/gnc/" # should be ignored
+ expect_start_message("organization_name" => "blue-origin")
+ events.run_started(run_status)
+ end
+
+ it "if the data collector org is unset we get 'chef_solo'" do
+ Chef::Config[:chef_server_url] = "https://spacex.rockets.local/organizations/gnc/" # should be ignored
+ expect_start_message("organization_name" => "chef_solo")
+ events.run_started(run_status)
+ end
+
+ it "sets the source" do
+ expect_start_message("source" => "chef_solo")
+ events.run_started(run_status)
end
end
- context "when the resource is not a nested resource" do
+ describe "in local mode" do
before do
- allow(reporter).to receive(:nested_resource?).with(new_resource).and_return(false)
+ Chef::Config[:local_mode] = true
+ Chef::Config[:data_collector][:server_url] = "https://nasa.rockets.local/organizations/sls"
end
- it "marks the current resource report as finished" do
- expect(resource_report).to receive(:finish)
- reporter.resource_completed(new_resource)
+ it "we get the data collector organization" do
+ Chef::Config[:data_collector][:organization] = "blue-origin"
+ Chef::Config[:chef_server_url] = "https://spacex.rockets.local/organizations/gnc/" # should be ignored
+ expect_start_message("organization_name" => "blue-origin")
+ events.run_started(run_status)
end
- it "nils out the current resource report" do
- expect(reporter).to receive(:clear_current_resource_report)
- reporter.resource_completed(new_resource)
+ it "if the data collector org is unset we get 'chef_solo'" do
+ Chef::Config[:chef_server_url] = "https://spacex.rockets.local/organizations/gnc/" # should be ignored
+ expect_start_message("organization_name" => "chef_solo")
+ events.run_started(run_status)
+ end
+
+ it "sets the source" do
+ expect_start_message("source" => "chef_solo")
+ events.run_started(run_status)
end
end
end
- end
- describe "#run_list_expanded" do
- it "sets the expanded run list" do
- reporter.run_list_expanded("test_run_list")
- expect(reporter.expanded_run_list).to eq("test_run_list")
- end
- end
+ describe "converge messages" do
+ before do
+ expect_start_message
+ events.run_started(run_status)
+ events.cookbook_compilation_start(run_context)
+ end
- describe "#run_list_expand_failed" do
- let(:node) { double("node") }
- let(:error_mapper) { double("error_mapper") }
- let(:exception) { double("exception") }
+ context "with no resources" do
+ let(:total_resource_count) { 0 }
+ let(:updated_resource_count) { 0 }
+ let(:resource_record) { [ ] }
+ let(:status) { "success" }
- it "updates the error description" do
- expect(Chef::Formatters::ErrorMapper).to receive(:run_list_expand_failed).with(
- node,
- exception
- ).and_return(error_mapper)
- expect(error_mapper).to receive(:for_json).and_return("error_description")
- expect(reporter).to receive(:update_error_description).with("error_description")
- reporter.run_list_expand_failed(node, exception)
- end
- end
+ before do
+ run_status.stop_clock
+ end
- describe "#cookbook_resolution_failed" do
- let(:error_mapper) { double("error_mapper") }
- let(:exception) { double("exception") }
- let(:expanded_run_list) { double("expanded_run_list") }
+ it_behaves_like "sends a converge message"
- it "updates the error description" do
- expect(Chef::Formatters::ErrorMapper).to receive(:cookbook_resolution_failed).with(
- expanded_run_list,
- exception
- ).and_return(error_mapper)
- expect(error_mapper).to receive(:for_json).and_return("error_description")
- expect(reporter).to receive(:update_error_description).with("error_description")
- reporter.cookbook_resolution_failed(expanded_run_list, exception)
- end
+ it "sets the policy_group" do
+ node.policy_group = "acceptionsal"
+ expect_converge_message("policy_group" => "acceptionsal")
+ send_run_failed_or_completed_event
+ end
- end
+ it "has a policy_name" do
+ node.policy_name = "webappdb"
+ expect_converge_message("policy_name" => "webappdb")
+ send_run_failed_or_completed_event
+ end
- describe "#cookbook_sync_failed" do
- let(:cookbooks) { double("cookbooks") }
- let(:error_mapper) { double("error_mapper") }
- let(:exception) { double("exception") }
+ it "collects deprecation messages" do
+ location = Chef::Log.caller_location
+ events.deprecation(Chef::Deprecated.create(:internal_api, "deprecation warning", location))
+ expect_converge_message("deprecations" => [{ location: location, message: "deprecation warning", url: "https://docs.chef.io/deprecations_internal_api.html" }])
+ send_run_failed_or_completed_event
+ end
+ end
- it "updates the error description" do
- expect(Chef::Formatters::ErrorMapper).to receive(:cookbook_sync_failed).with(
- cookbooks,
- exception
- ).and_return(error_mapper)
- expect(error_mapper).to receive(:for_json).and_return("error_description")
- expect(reporter).to receive(:update_error_description).with("error_description")
- reporter.cookbook_sync_failed(cookbooks, exception)
- end
- end
+ context "when the run contains a file resource that is up-to-date" do
+ let(:total_resource_count) { 1 }
+ let(:updated_resource_count) { 0 }
+ let(:resource_record) { [ resource_record_for(current_resource, new_resource, :create, "up-to-date", "1234") ] }
+ let(:status) { "success" }
- describe "#disable_reporter_on_error" do
- context "when no exception is raise by the block" do
- it "does not disable the reporter" do
- expect(reporter).not_to receive(:disable_data_collector_reporter)
- reporter.send(:disable_reporter_on_error) { true }
- end
+ before do
+ events.resource_action_start(new_resource, :create)
+ events.resource_current_state_loaded(new_resource, :create, current_resource)
+ events.resource_up_to_date(new_resource, :create)
+ new_resource.instance_variable_set(:@elapsed_time, 1.2345)
+ events.resource_completed(new_resource)
+ events.converge_complete
+ run_status.stop_clock
+ end
- it "does not raise an exception" do
- expect { reporter.send(:disable_reporter_on_error) { true } }.not_to raise_error
+ it_behaves_like "sends a converge message"
end
- end
- context "when an unexpected exception is raised by the block" do
- it "re-raises the exception" do
- expect { reporter.send(:disable_reporter_on_error) { raise "bummer" } }.to raise_error(RuntimeError)
- end
- end
+ context "when the run contains a file resource that is updated" do
+ let(:total_resource_count) { 1 }
+ let(:updated_resource_count) { 1 }
+ let(:resource_record) { [ resource_record_for(current_resource, new_resource, :create, "updated", "1234") ] }
+ let(:status) { "success" }
- [ Timeout::Error, Errno::EINVAL, Errno::ECONNRESET,
- Errno::ECONNREFUSED, EOFError, Net::HTTPBadResponse,
- Net::HTTPHeaderSyntaxError, Net::ProtocolError, OpenSSL::SSL::SSLError,
- Errno::EHOSTDOWN ].each do |exception_class|
- context "when the block raises a #{exception_class} exception" do
- it "disables the reporter" do
- expect(reporter).to receive(:disable_data_collector_reporter)
- reporter.send(:disable_reporter_on_error) { raise exception_class.new("bummer") }
+ before do
+ events.resource_action_start(new_resource, :create)
+ events.resource_current_state_loaded(new_resource, :create, current_resource)
+ events.resource_updated(new_resource, :create)
+ new_resource.instance_variable_set(:@elapsed_time, 1.2345)
+ events.resource_completed(new_resource)
+ events.converge_complete
+ run_status.stop_clock
end
- context "when raise-on-failure is enabled" do
- it "logs an error and raises" do
- Chef::Config[:data_collector][:raise_on_failure] = true
- expect(Chef::Log).to receive(:error)
- expect { reporter.send(:disable_reporter_on_error) { raise exception_class.new("bummer") } }.to raise_error(exception_class)
- end
+ it_behaves_like "sends a converge message"
+ end
+
+ context "When there is an embedded resource, it includes the sub-resource in the report" do
+ let(:total_resource_count) { 2 }
+ let(:updated_resource_count) { 2 }
+ let(:implementation_resource) do
+ r = Chef::Resource::CookbookFile.new("/preseed-file.txt")
+ r.cookbook_name = cookbook_name
+ r.recipe_name = recipe_name
+ allow(r).to receive(:cookbook_version).and_return(cookbook_version)
+ r
end
+ let(:resource_record) { [ resource_record_for(implementation_resource, implementation_resource, :create, "updated", "2345"), resource_record_for(current_resource, new_resource, :create, "updated", "1234") ] }
+ let(:status) { "success" }
- context "when raise-on-failure is disabled" do
- it "logs an info message and does not raise an exception" do
- Chef::Config[:data_collector][:raise_on_failure] = false
- expect(Chef::Log).to receive(:info)
- expect { reporter.send(:disable_reporter_on_error) { raise exception_class.new("bummer") } }.not_to raise_error
- end
+ before do
+ events.resource_action_start(new_resource, :create)
+ events.resource_current_state_loaded(new_resource, :create, current_resource)
+
+ events.resource_action_start(implementation_resource , :create)
+ events.resource_current_state_loaded(implementation_resource, :create, implementation_resource)
+ events.resource_updated(implementation_resource, :create)
+ implementation_resource.instance_variable_set(:@elapsed_time, 2.3456)
+ events.resource_completed(implementation_resource)
+
+ events.resource_updated(new_resource, :create)
+ new_resource.instance_variable_set(:@elapsed_time, 1.2345)
+ events.resource_completed(new_resource)
+ events.converge_complete
+ run_status.stop_clock
end
- end
- end
- end
- describe "#validate_data_collector_server_url!" do
- context "when server_url is empty" do
- it "raises an exception" do
- Chef::Config[:data_collector][:server_url] = ""
- expect { reporter.send(:validate_data_collector_server_url!) }.to raise_error(Chef::Exceptions::ConfigurationError)
+ it_behaves_like "sends a converge message"
end
- end
- context "when server_url is omitted but output_locations is specified" do
- it "does not an exception" do
- Chef::Config[:data_collector][:output_locations] = ["http://data_collector", "/tmp/data_collector.json"]
- expect { reporter.send(:validate_data_collector_server_url!) }.not_to raise_error(Chef::Exceptions::ConfigurationError)
- end
- end
+ context "when the run contains a file resource that is skipped due to a block conditional" do
+ let(:total_resource_count) { 1 }
+ let(:updated_resource_count) { 0 }
+ let(:resource_record) do
+ rec = resource_record_for(current_resource, new_resource, :create, "skipped", "1234")
+ rec["conditional"] = "not_if { #code block }" # FIXME: "#code block" is poor, is there some way to fix this?
+ [ rec ]
+ end
+ let(:status) { "success" }
- context "when server_url is not empty" do
- context "when server_url is an invalid URI" do
- it "raises an exception" do
- Chef::Config[:data_collector][:server_url] = "this is not a URI"
- expect { reporter.send(:validate_data_collector_server_url!) }.to raise_error(Chef::Exceptions::ConfigurationError)
+ before do
+ conditional = (new_resource.not_if { true }).first
+ events.resource_action_start(new_resource, :create)
+ events.resource_current_state_loaded(new_resource, :create, current_resource)
+ events.resource_skipped(new_resource, :create, conditional)
+ new_resource.instance_variable_set(:@elapsed_time, 1.2345)
+ events.resource_completed(new_resource)
+ events.converge_complete
+ run_status.stop_clock
end
- end
- context "when server_url is a valid URI" do
- context "when server_url is a URI with no host" do
- it "raises an exception" do
- Chef::Config[:data_collector][:server_url] = "/file/uri.txt"
- expect { reporter.send(:validate_data_collector_server_url!) }.to raise_error(Chef::Exceptions::ConfigurationError)
- end
+ it_behaves_like "sends a converge message"
+ end
+ context "when the run contains a file resource that is skipped due to a string conditional" do
+ let(:total_resource_count) { 1 }
+ let(:updated_resource_count) { 0 }
+ let(:resource_record) do
+ rec = resource_record_for(current_resource, new_resource, :create, "skipped", "1234")
+ rec["conditional"] = 'not_if "true"'
+ [ rec ]
end
+ let(:status) { "success" }
- context "when server_url is a URI with a valid host" do
- it "does not an exception" do
- Chef::Config[:data_collector][:server_url] = "http://www.google.com/data-collector"
- expect { reporter.send(:validate_data_collector_server_url!) }.not_to raise_error
- end
+ before do
+ conditional = (new_resource.not_if "true").first
+ events.resource_action_start(new_resource, :create)
+ events.resource_current_state_loaded(new_resource, :create, current_resource)
+ events.resource_skipped(new_resource, :create, conditional)
+ new_resource.instance_variable_set(:@elapsed_time, 1.2345)
+ events.resource_completed(new_resource)
+ events.converge_complete
+ run_status.stop_clock
end
- end
- end
- end
- describe "#validate_data_collector_output_locations!" do
- context "when output_locations is empty" do
- it "raises an exception" do
- Chef::Config[:data_collector][:output_locations] = {}
- expect { reporter.send(:validate_data_collector_output_locations!) }.to raise_error(Chef::Exceptions::ConfigurationError)
+ it_behaves_like "sends a converge message"
end
- end
- context "when valid output_locations are provided" do
- it "does not raise an exception" do
- expect(reporter).to receive(:open).with("data_collection.json", "a")
- Chef::Config[:data_collector][:output_locations] = { urls: ["http://data_collector"], files: ["data_collection.json"] }
- expect { reporter.send(:validate_data_collector_output_locations!) }.not_to raise_error(Chef::Exceptions::ConfigurationError)
- end
- end
+ context "when the run contains a file resource that threw an exception" do
+ let(:exception) { Exception.new("imperial to metric conversion error") }
+ let(:error_description) { Chef::Formatters::ErrorMapper.resource_failed(new_resource, :create, exception).for_json }
+ let(:total_resource_count) { 1 }
+ let(:updated_resource_count) { 0 }
+ let(:resource_record) do
+ rec = resource_record_for(current_resource, new_resource, :create, "failed", "1234")
+ rec["error_message"] = "imperial to metric conversion error"
+ [ rec ]
+ end
+ let(:status) { "failure" }
+
+ before do
+ exception.set_backtrace(caller)
+ events.resource_action_start(new_resource, :create)
+ events.resource_current_state_loaded(new_resource, :create, current_resource)
+ events.resource_failed(new_resource, :create, exception)
+ new_resource.instance_variable_set(:@elapsed_time, 1.2345)
+ events.resource_completed(new_resource)
+ events.converge_complete
+ run_status.stop_clock
+ run_status.exception = exception
+ end
- context "when output_locations contains an invalid URI" do
- it "raises an exception" do
- Chef::Config[:data_collector][:output_locations] = { urls: ["this is not a url"], files: ["/tmp/data_collection.json"] }
- expect { reporter.send(:validate_data_collector_output_locations!) }.to raise_error(Chef::Exceptions::ConfigurationError)
+ it_behaves_like "sends a converge message"
end
- end
- end
- describe "#detect_unprocessed_resources" do
- context "when resources do not override core methods" do
- it "adds resource reports for any resources that have not yet been processed" do
- resource_a = Chef::Resource::Service.new("processed service")
- resource_b = Chef::Resource::Service.new("unprocessed service")
+ context "when the run contains a file resource that threw an exception in load_current_resource" do
+ let(:exception) { Exception.new("imperial to metric conversion error") }
+ let(:error_description) { Chef::Formatters::ErrorMapper.resource_failed(new_resource, :create, exception).for_json }
+ let(:total_resource_count) { 1 }
+ let(:updated_resource_count) { 0 }
+ let(:resource_record) do
+ rec = resource_record_for(current_resource, new_resource, :create, "failed", "1234")
+ rec["before"] = {}
+ rec["error_message"] = "imperial to metric conversion error"
+ [ rec ]
+ end
+ let(:status) { "failure" }
- resource_a.action = [ :enable, :start ]
- resource_b.action = :start
+ before do
+ exception.set_backtrace(caller)
+ events.resource_action_start(new_resource, :create)
+ # resource_current_state_loaded is skipped
+ events.resource_failed(new_resource, :create, exception)
+ new_resource.instance_variable_set(:@elapsed_time, 1.2345)
+ events.resource_completed(new_resource)
+ events.converge_failed(exception)
+ run_status.stop_clock
+ run_status.exception = exception
+ end
- run_context = Chef::RunContext.new(Chef::Node.new, Chef::CookbookCollection.new, nil)
- run_context.resource_collection.insert(resource_a)
- run_context.resource_collection.insert(resource_b)
+ it_behaves_like "sends a converge message"
+ end
- allow(reporter).to receive(:run_context).and_return(run_context)
+ context "when the resource collection contains a resource that was unproccesed due to prior errors" do
+ let(:exception) { Exception.new("imperial to metric conversion error") }
+ let(:error_description) { Chef::Formatters::ErrorMapper.resource_failed(new_resource, :create, exception).for_json }
+ let(:total_resource_count) { 2 }
+ let(:updated_resource_count) { 0 }
+ let(:unprocessed_resource) do
+ res = Chef::Resource::Service.new("unprocessed service")
+ res.cookbook_name = cookbook_name
+ res.recipe_name = recipe_name
+ allow(res).to receive(:cookbook_version).and_return(cookbook_version)
+ res
+ end
+ let(:resource_record) do
+ rec1 = resource_record_for(current_resource, new_resource, :create, "failed", "1234")
+ rec1["error_message"] = "imperial to metric conversion error"
+ rec2 = resource_record_for(nil, unprocessed_resource, :nothing, "unprocessed", "")
+ rec2["before"] = {}
+ [ rec1, rec2 ]
+ end
+ let(:status) { "failure" }
- # process the actions for resource_a, but not resource_b
- reporter.resource_up_to_date(resource_a, :enable)
- reporter.resource_completed(resource_a)
- reporter.resource_up_to_date(resource_a, :start)
- reporter.resource_completed(resource_a)
- expect(reporter.all_resource_reports.size).to eq(2)
+ before do
+ run_context.resource_collection << new_resource
+ run_context.resource_collection << unprocessed_resource
+ exception.set_backtrace(caller)
+ events.resource_action_start(new_resource, :create)
+ events.resource_current_state_loaded(new_resource, :create, current_resource)
+ events.resource_failed(new_resource, :create, exception)
+ new_resource.instance_variable_set(:@elapsed_time, 1.2345)
+ events.resource_completed(new_resource)
+ new_resource.executed_by_runner = true
+ events.converge_failed(exception)
+ run_status.stop_clock
+ run_status.exception = exception
+ end
- # detect unprocessed resources, which should find that resource_b has not yet been processed
- reporter.send(:detect_unprocessed_resources)
- expect(reporter.all_resource_reports.size).to eq(3)
+ it_behaves_like "sends a converge message"
end
- end
-
- context "when a resource overrides a core method, such as #hash" do
- it "does not raise an exception" do
- resource_a = Chef::Resource::Service.new("processed service")
- resource_b = Chef::Resource::Service.new("unprocessed service")
- resource_a.action = :start
- resource_b.action = :start
+ context "when cookbook resolution fails" do
+ let(:exception) { Exception.new("imperial to metric conversion error") }
+ let(:error_description) { Chef::Formatters::ErrorMapper.cookbook_resolution_failed(expansion, exception).for_json }
+ let(:total_resource_count) { 0 }
+ let(:updated_resource_count) { 0 }
+ let(:status) { "failure" }
- run_context = Chef::RunContext.new(Chef::Node.new, Chef::CookbookCollection.new, nil)
- run_context.resource_collection.insert(resource_a)
- run_context.resource_collection.insert(resource_b)
+ before do
+ events.cookbook_resolution_failed(expansion, exception)
+ run_status.stop_clock
+ run_status.exception = exception
+ end
- allow(reporter).to receive(:run_context).and_return(run_context)
+ it_behaves_like "sends a converge message"
+ end
- # override the #hash method on resource_a to return a String instead of
- # a Fixnum. Without the fix in chef/chef#5604, this would raise an
- # exception when getting added to the Set/Hash.
- resource_a.define_singleton_method(:hash) { "a string" }
+ context "When cookbook synchronization fails" do
+ let(:exception) { Exception.new("imperial to metric conversion error") }
+ let(:error_description) { Chef::Formatters::ErrorMapper.cookbook_sync_failed({}, exception).for_json }
+ let(:total_resource_count) { 0 }
+ let(:updated_resource_count) { 0 }
+ let(:status) { "failure" }
- # process the actions for resource_a, but not resource_b
- reporter.resource_up_to_date(resource_a, :start)
- reporter.resource_completed(resource_a)
+ before do
+ events.cookbook_sync_failed(expansion, exception)
+ run_status.stop_clock
+ run_status.exception = exception
+ end
- expect { reporter.send(:detect_unprocessed_resources) }.not_to raise_error
+ it_behaves_like "sends a converge message"
end
+
end
end
end
diff --git a/spec/unit/event_dispatch/dispatcher_spec.rb b/spec/unit/event_dispatch/dispatcher_spec.rb
index 5061a9845f..1db43ad740 100644
--- a/spec/unit/event_dispatch/dispatcher_spec.rb
+++ b/spec/unit/event_dispatch/dispatcher_spec.rb
@@ -1,7 +1,7 @@
#
# Author:: Daniel DeLeo (<dan@chef.io>)
#
-# Copyright:: Copyright 2015-2016, Chef Software, Inc.
+# Copyright:: Copyright 2015-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -43,9 +43,9 @@ describe Chef::EventDispatch::Dispatcher do
it "forwards events to the subscribed event sink" do
# the events all have different arity and such so we just hit a few different events:
-
- expect(event_sink).to receive(:run_start).with("12.4.0")
- dispatcher.run_start("12.4.0")
+ run_status = Chef::RunStatus.new({}, {})
+ expect(event_sink).to receive(:run_start).with("12.4.0", run_status)
+ dispatcher.run_start("12.4.0", run_status)
cookbook_version = double("cookbook_version")
expect(event_sink).to receive(:synchronized_cookbook).with("apache2", cookbook_version)
@@ -119,4 +119,51 @@ describe Chef::EventDispatch::Dispatcher do
end
end
end
+
+ context "events that queue events" do
+ class Accumulator
+ def self.sequence
+ @secuence ||= []
+ end
+ end
+
+ let(:event_sink_1) do
+ Class.new(Chef::EventDispatch::Base) do
+ def synchronized_cookbook(dispatcher, arg)
+ dispatcher.enqueue(:event_two, arg)
+ Accumulator.sequence << [ :sink_1_event_1, arg ]
+ end
+
+ def event_two(arg)
+ Accumulator.sequence << [ :sink_1_event_2, arg ]
+ end
+ end.new
+ end
+ let(:event_sink_2) do
+ Class.new(Chef::EventDispatch::Base) do
+ def synchronized_cookbook(dispatcher, arg)
+ Accumulator.sequence << [ :sink_2_event_1, arg ]
+ end
+
+ def event_two(arg)
+ Accumulator.sequence << [ :sink_2_event_2, arg ]
+ end
+ end.new
+ end
+
+ before do
+ dispatcher.register(event_sink_1)
+ dispatcher.register(event_sink_2)
+ end
+
+ it "runs the events in the correct order without interleaving the enqueued event" do
+ dispatcher.synchronized_cookbook(dispatcher, "two")
+ expect(Accumulator.sequence).to eql([
+ [:sink_1_event_1, "two"], # the call to enqueue the event happens here
+ [:sink_2_event_1, "two"], # event 1 fully finishes
+ [:sink_1_event_2, "two"],
+ [:sink_2_event_2, "two"], # then event 2 runs and finishes
+ ])
+ end
+ end
end
diff --git a/spec/unit/node_spec.rb b/spec/unit/node_spec.rb
index 37dc27ec0f..752871e2e9 100644
--- a/spec/unit/node_spec.rb
+++ b/spec/unit/node_spec.rb
@@ -1,6 +1,6 @@
#
# Author:: Adam Jacob (<adam@chef.io>)
-# Copyright:: Copyright 2008-2018, Chef Software Inc.
+# Copyright:: Copyright 2008-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -953,14 +953,6 @@ describe Chef::Node do
expect(node.automatic_attrs[:platform_version]).to eq("23.42")
end
- it "sets the chef guid attribute correctly" do
- guid = Chef::Config[:chef_guid]
- Chef::Config[:chef_guid] = "test-guid-guid"
- node.consume_external_attrs(@ohai_data, {})
- expect(node.automatic_attrs[:chef_guid]).to eq("test-guid-guid")
- Chef::Config[:chef_guid] = guid
- end
-
it "consumes the run list from provided json attributes" do
node.consume_external_attrs(@ohai_data, { "run_list" => ["recipe[unicorn]"] })
expect(node.run_list).to eq(["recipe[unicorn]"])
diff --git a/spec/unit/resource_reporter_spec.rb b/spec/unit/resource_reporter_spec.rb
index cec931dd70..2272c83967 100644
--- a/spec/unit/resource_reporter_spec.rb
+++ b/spec/unit/resource_reporter_spec.rb
@@ -1,9 +1,9 @@
-#
+
# Author:: Daniel DeLeo (<dan@chef.io>)
# Author:: Prajakta Purohit (<prajakta@chef.io>)
# Author:: Tyler Cloke (<tyler@chef.io>)
#
-# Copyright:: Copyright 2012-2017, Chef Software Inc.
+# Copyright:: Copyright 2012-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,57 +24,70 @@ require "chef/resource_reporter"
require "socket"
describe Chef::ResourceReporter do
- before(:all) do
- @reporting_toggle_default = Chef::Config[:enable_reporting]
+ before(:each) do
Chef::Config[:enable_reporting] = true
end
- after(:all) do
- Chef::Config[:enable_reporting] = @reporting_toggle_default
- end
+ let(:node) { Chef::Node.new }
+
+ let(:rest_client) { double("Chef::ServerAPI (mock)") }
+
+ let(:resource_reporter) { Chef::ResourceReporter.new(rest_client) }
+
+ let(:new_resource) { Chef::Resource::File.new("/tmp/a-file.txt") }
+
+ let(:current_resource) { Chef::Resource::File.new("/tmp/a-file.txt") }
+
+ let(:events) { Chef::EventDispatch::Dispatcher.new }
+
+ let(:run_context) { Chef::RunContext.new(node, {}, events) }
+
+ let(:run_status) { Chef::RunStatus.new(node, events) }
+
+ let(:start_time) { Time.new }
+
+ let(:end_time) { Time.new + 20 }
+
+ let(:run_list) { Chef::RunList.new }
+
+ let(:run_id) { run_status.run_id }
+
+ let(:expansion) { Chef::RunList::RunListExpansion.new("_default", run_list.run_list_items) }
+
+ let(:cookbook_name) { "monkey" }
+
+ let(:cookbook_version) { double("Cookbook::Version", version: "1.2.3") }
+
+ let(:action_collection) { Chef::ActionCollection.new(events) }
before do
- @node = Chef::Node.new
- @node.name("spitfire")
- @rest_client = double("Chef::ServerAPI (mock)")
- allow(@rest_client).to receive(:post).and_return(true)
- @resource_reporter = Chef::ResourceReporter.new(@rest_client)
- @new_resource = Chef::Resource::File.new("/tmp/a-file.txt")
- @cookbook_name = "monkey"
- @new_resource.cookbook_name = @cookbook_name
- @cookbook_version = double("Cookbook::Version", version: "1.2.3")
- allow(@new_resource).to receive(:cookbook_version).and_return(@cookbook_version)
- @current_resource = Chef::Resource::File.new("/tmp/a-file.txt")
- @start_time = Time.new
- @end_time = Time.new + 20
- @events = Chef::EventDispatch::Dispatcher.new
- @run_context = Chef::RunContext.new(@node, {}, @events)
- @run_status = Chef::RunStatus.new(@node, @events)
- @run_list = Chef::RunList.new
- @run_list << "recipe[lobster]" << "role[rage]" << "recipe[fist]"
- @expansion = Chef::RunList::RunListExpansion.new("_default", @run_list.run_list_items)
- @run_id = @run_status.run_id
- allow(Time).to receive(:now).and_return(@start_time, @end_time)
+ node.name("spitfire")
+ allow(rest_client).to receive(:post).and_return(true)
+ new_resource.cookbook_name = cookbook_name
+ allow(new_resource).to receive(:cookbook_version).and_return(cookbook_version)
+ run_list << "recipe[lobster]" << "role[rage]" << "recipe[fist]"
+ allow(Time).to receive(:now).and_return(start_time, end_time)
+ events.register(action_collection)
+ events.register(resource_reporter)
+ events.cookbook_compilation_start(run_context)
end
context "when first created" do
it "has no updated resources" do
- expect(@resource_reporter.updated_resources.size).to eq(0)
+ expect(resource_reporter.updated_resources.count).to eq(0)
end
it "reports a successful run" do
- expect(@resource_reporter.status).to eq("success")
+ expect(resource_reporter.status).to eq("success")
end
it "assumes the resource history feature is supported" do
- expect(@resource_reporter.reporting_enabled?).to be_truthy
+ expect(resource_reporter.send(:reporting_enabled?)).to be_truthy
end
it "should have no error_descriptions" do
- expect(@resource_reporter.error_descriptions).to eq({})
- # @resource_reporter.error_descriptions.should be_empty
- # @resource_reporter.should have(0).error_descriptions
+ expect(resource_reporter.error_descriptions).to eq({})
end
end
@@ -86,49 +99,50 @@ describe Chef::ResourceReporter do
it "reports a successful run" do
skip "refactor how node gets set."
- expect(@resource_reporter.status).to eq("success")
+ expect(resource_reporter.status).to eq("success")
end
end
context "when chef fails" do
before do
- allow(@rest_client).to receive(:raw_request).and_return({ "result" => "ok" })
- allow(@rest_client).to receive(:post).and_return({ "uri" => "https://example.com/reports/nodes/spitfire/runs/#{@run_id}" })
+ allow(rest_client).to receive(:raw_request).and_return({ "result" => "ok" })
+ allow(rest_client).to receive(:post).and_return({ "uri" => "https://example.com/reports/nodes/spitfire/runs/#{@run_id}" })
end
context "before converging any resources" do
+ let(:exception) { Exception.new }
+
before do
- @resource_reporter.run_started(@run_status)
- @exception = Exception.new
- @resource_reporter.run_failed(@exception)
+ resource_reporter.run_started(run_status)
+ resource_reporter.run_failed(exception)
end
it "sets the run status to 'failure'" do
- expect(@resource_reporter.status).to eq("failure")
+ expect(resource_reporter.status).to eq("failure")
end
it "keeps the exception data" do
- expect(@resource_reporter.exception).to eq(@exception)
+ expect(resource_reporter.exception).to eq(exception)
end
end
context "when a resource fails before loading current state" do
before do
- @exception = Exception.new
- @exception.set_backtrace(caller)
- @resource_reporter.resource_action_start(@new_resource, :create)
- @resource_reporter.resource_failed(@new_resource, :create, @exception)
- @resource_reporter.resource_completed(@new_resource)
+ exception = Exception.new
+ exception.set_backtrace(caller)
+ events.resource_action_start(new_resource, :create)
+ events.resource_failed(new_resource, :create, exception)
+ events.resource_completed(new_resource)
end
it "collects the resource as an updated resource" do
- expect(@resource_reporter.updated_resources.size).to eq(1)
+ expect(resource_reporter.updated_resources.count).to eq(1)
end
it "collects the desired state of the resource" do
- update_record = @resource_reporter.updated_resources.first
- expect(update_record.new_resource).to eq(@new_resource)
+ update_record = resource_reporter.updated_resources.first
+ expect(update_record.new_resource).to eq(new_resource)
end
end
@@ -137,58 +151,73 @@ describe Chef::ResourceReporter do
context "once the a resource's current state is loaded" do
before do
- @resource_reporter.resource_action_start(@new_resource, :create)
- @resource_reporter.resource_current_state_loaded(@new_resource, :create, @current_resource)
+ events.resource_action_start(new_resource, :create)
+ events.resource_current_state_loaded(new_resource, :create, current_resource)
end
context "and the resource was not updated" do
before do
- @resource_reporter.resource_up_to_date(@new_resource, :create)
+ events.resource_up_to_date(new_resource, :create)
+ events.resource_completed(new_resource)
+ end
+
+ it "has no updated resources" do
+ expect(resource_reporter.updated_resources.count).to eq(0)
+ end
+ end
+
+ context "and the resource was skipped" do
+ before do
+ conditional = nil
+ events.resource_skipped(new_resource, :create, conditional)
+ events.resource_completed(new_resource)
end
it "has no updated resources" do
- expect(@resource_reporter.updated_resources.size).to eq(0)
+ expect(resource_reporter.updated_resources.count).to eq(0)
end
end
context "and the resource was updated" do
before do
- @new_resource.content("this is the old content")
- @current_resource.content("this is the new hotness")
- @resource_reporter.resource_updated(@new_resource, :create)
- @resource_reporter.resource_completed(@new_resource)
+ new_resource.content("this is the old content")
+ current_resource.content("this is the new hotness")
+ events.resource_updated(new_resource, :create)
+ events.resource_completed(new_resource)
end
it "collects the updated resource" do
- expect(@resource_reporter.updated_resources.size).to eq(1)
+ expect(resource_reporter.updated_resources.count).to eq(1)
end
it "collects the old state of the resource" do
- update_record = @resource_reporter.updated_resources.first
- expect(update_record.current_resource).to eq(@current_resource)
+ update_record = resource_reporter.updated_resources.first
+ expect(update_record.current_resource).to eq(current_resource)
end
it "collects the new state of the resource" do
- update_record = @resource_reporter.updated_resources.first
- expect(update_record.new_resource).to eq(@new_resource)
+ update_record = resource_reporter.updated_resources.first
+ expect(update_record.new_resource).to eq(new_resource)
end
context "and a subsequent resource fails before loading current resource" do
+ let(:next_new_resource) { Chef::Resource::Service.new("apache2") }
+
before do
- @next_new_resource = Chef::Resource::Service.new("apache2")
- @exception = Exception.new
- @exception.set_backtrace(caller)
- @resource_reporter.resource_failed(@next_new_resource, :create, @exception)
- @resource_reporter.resource_completed(@next_new_resource)
+ exception = Exception.new
+ exception.set_backtrace(caller)
+ events.resource_action_start(next_new_resource, :create)
+ events.resource_failed(next_new_resource, :create, exception)
+ events.resource_completed(next_new_resource)
end
it "collects the desired state of the failed resource" do
- failed_resource_update = @resource_reporter.updated_resources.last
- expect(failed_resource_update.new_resource).to eq(@next_new_resource)
+ failed_resource_update = resource_reporter.updated_resources.last
+ expect(failed_resource_update.new_resource).to eq(next_new_resource)
end
it "does not have the current state of the failed resource" do
- failed_resource_update = @resource_reporter.updated_resources.last
+ failed_resource_update = resource_reporter.updated_resources.last
expect(failed_resource_update.current_resource).to be_nil
end
end
@@ -200,56 +229,56 @@ describe Chef::ResourceReporter do
# used for implementation.
context "and a nested resource is updated" do
before do
- @implementation_resource = Chef::Resource::CookbookFile.new("/preseed-file.txt")
- @resource_reporter.resource_action_start(@implementation_resource , :create)
- @resource_reporter.resource_current_state_loaded(@implementation_resource, :create, @implementation_resource)
- @resource_reporter.resource_updated(@implementation_resource, :create)
- @resource_reporter.resource_completed(@implementation_resource)
- @resource_reporter.resource_updated(@new_resource, :create)
- @resource_reporter.resource_completed(@new_resource)
+ implementation_resource = Chef::Resource::CookbookFile.new("/preseed-file.txt")
+ events.resource_action_start(implementation_resource , :create)
+ events.resource_current_state_loaded(implementation_resource, :create, implementation_resource)
+ events.resource_updated(implementation_resource, :create)
+ events.resource_completed(implementation_resource)
+ events.resource_updated(new_resource, :create)
+ events.resource_completed(new_resource)
end
it "does not collect data about the nested resource" do
- expect(@resource_reporter.updated_resources.size).to eq(1)
+ expect(resource_reporter.updated_resources.count).to eq(1)
end
end
context "and a nested resource runs but is not updated" do
before do
- @implementation_resource = Chef::Resource::CookbookFile.new("/preseed-file.txt")
- @resource_reporter.resource_action_start(@implementation_resource , :create)
- @resource_reporter.resource_current_state_loaded(@implementation_resource, :create, @implementation_resource)
- @resource_reporter.resource_up_to_date(@implementation_resource, :create)
- @resource_reporter.resource_completed(@implementation_resource)
- @resource_reporter.resource_updated(@new_resource, :create)
- @resource_reporter.resource_completed(@new_resource)
+ implementation_resource = Chef::Resource::CookbookFile.new("/preseed-file.txt")
+ events.resource_action_start(implementation_resource , :create)
+ events.resource_current_state_loaded(implementation_resource, :create, implementation_resource)
+ events.resource_up_to_date(implementation_resource, :create)
+ events.resource_completed(implementation_resource)
+ events.resource_updated(new_resource, :create)
+ events.resource_completed(new_resource)
end
it "does not collect data about the nested resource" do
- expect(@resource_reporter.updated_resources.size).to eq(1)
+ expect(resource_reporter.updated_resources.count).to eq(1)
end
end
context "and the resource failed to converge" do
before do
- @exception = Exception.new
- @exception.set_backtrace(caller)
- @resource_reporter.resource_failed(@new_resource, :create, @exception)
- @resource_reporter.resource_completed(@new_resource)
+ exception = Exception.new
+ exception.set_backtrace(caller)
+ events.resource_failed(new_resource, :create, exception)
+ events.resource_completed(new_resource)
end
it "collects the resource as an updated resource" do
- expect(@resource_reporter.updated_resources.size).to eq(1)
+ expect(resource_reporter.updated_resources.count).to eq(1)
end
it "collects the desired state of the resource" do
- update_record = @resource_reporter.updated_resources.first
- expect(update_record.new_resource).to eq(@new_resource)
+ update_record = resource_reporter.updated_resources.first
+ expect(update_record.new_resource).to eq(new_resource)
end
it "collects the current state of the resource" do
- update_record = @resource_reporter.updated_resources.first
- expect(update_record.current_resource).to eq(@current_resource)
+ update_record = resource_reporter.updated_resources.first
+ expect(update_record.current_resource).to eq(current_resource)
end
end
@@ -259,10 +288,10 @@ describe Chef::ResourceReporter do
describe "when generating a report for the server" do
before do
- allow(@rest_client).to receive(:raw_request).and_return({ "result" => "ok" })
- allow(@rest_client).to receive(:post).and_return({ "uri" => "https://example.com/reports/nodes/spitfire/runs/#{@run_id}" })
+ allow(rest_client).to receive(:raw_request).and_return({ "result" => "ok" })
+ allow(rest_client).to receive(:post).and_return({ "uri" => "https://example.com/reports/nodes/spitfire/runs/#{@run_id}" })
- @resource_reporter.run_started(@run_status)
+ resource_reporter.run_started(run_status)
end
context "when the new_resource is sensitive" do
@@ -271,12 +300,12 @@ describe Chef::ResourceReporter do
@execute_resource.name("sensitive-resource")
@execute_resource.command('echo "password: SECRET"')
@execute_resource.sensitive(true)
- @resource_reporter.resource_action_start(@execute_resource, :run)
- @resource_reporter.resource_current_state_loaded(@execute_resource, :run, @current_resource)
- @resource_reporter.resource_updated(@execute_resource, :run)
- @resource_reporter.resource_completed(@execute_resource)
- @run_status.stop_clock
- @report = @resource_reporter.prepare_run_data
+ events.resource_action_start(@execute_resource, :run)
+ events.resource_current_state_loaded(@execute_resource, :run, current_resource)
+ events.resource_updated(@execute_resource, :run)
+ events.resource_completed(@execute_resource)
+ run_status.stop_clock
+ @report = resource_reporter.prepare_run_data
@first_update_report = @report["resources"].first
end
@@ -296,12 +325,12 @@ describe Chef::ResourceReporter do
allow(@bad_resource).to receive(:name).and_return(nil)
allow(@bad_resource).to receive(:identity).and_return(nil)
allow(@bad_resource).to receive(:path).and_return(nil)
- @resource_reporter.resource_action_start(@bad_resource, :create)
- @resource_reporter.resource_current_state_loaded(@bad_resource, :create, @current_resource)
- @resource_reporter.resource_updated(@bad_resource, :create)
- @resource_reporter.resource_completed(@bad_resource)
- @run_status.stop_clock
- @report = @resource_reporter.prepare_run_data
+ events.resource_action_start(@bad_resource, :create)
+ events.resource_current_state_loaded(@bad_resource, :create, current_resource)
+ events.resource_updated(@bad_resource, :create)
+ events.resource_completed(@bad_resource)
+ run_status.stop_clock
+ @report = resource_reporter.prepare_run_data
@first_update_report = @report["resources"].first
end
@@ -320,12 +349,12 @@ describe Chef::ResourceReporter do
allow(@bad_resource).to receive(:name).and_return({ foo: :bar })
allow(@bad_resource).to receive(:identity).and_return({ foo: :bar })
allow(@bad_resource).to receive(:path).and_return({ foo: :bar })
- @resource_reporter.resource_action_start(@bad_resource, :create)
- @resource_reporter.resource_current_state_loaded(@bad_resource, :create, @current_resource)
- @resource_reporter.resource_updated(@bad_resource, :create)
- @resource_reporter.resource_completed(@bad_resource)
- @run_status.stop_clock
- @report = @resource_reporter.prepare_run_data
+ events.resource_action_start(@bad_resource, :create)
+ events.resource_current_state_loaded(@bad_resource, :create, current_resource)
+ events.resource_updated(@bad_resource, :create)
+ events.resource_completed(@bad_resource)
+ run_status.stop_clock
+ @report = resource_reporter.prepare_run_data
@first_update_report = @report["resources"].first
end
# Ruby 1.8.7 flattens out hash to string using join instead of inspect, resulting in
@@ -377,12 +406,12 @@ describe Chef::ResourceReporter do
# "status" : "success"
# "data" : ""
# }
- @resource_reporter.resource_action_start(new_resource, :create)
- @resource_reporter.resource_current_state_loaded(new_resource, :create, current_resource)
- @resource_reporter.resource_updated(new_resource, :create)
- @resource_reporter.resource_completed(new_resource)
- @run_status.stop_clock
- @report = @resource_reporter.prepare_run_data
+ events.resource_action_start(new_resource, :create)
+ events.resource_current_state_loaded(new_resource, :create, current_resource)
+ events.resource_updated(new_resource, :create)
+ events.resource_completed(new_resource)
+ run_status.stop_clock
+ @report = resource_reporter.prepare_run_data
@first_update_report = @report["resources"].first
end
@@ -428,7 +457,7 @@ describe Chef::ResourceReporter do
it "includes the cookbook name of the resource" do
expect(@first_update_report).to have_key("cookbook_name")
- expect(@first_update_report["cookbook_name"]).to eq(@cookbook_name)
+ expect(@first_update_report["cookbook_name"]).to eq(cookbook_name)
end
it "includes the cookbook version of the resource" do
@@ -448,7 +477,7 @@ describe Chef::ResourceReporter do
it "includes the run_list" do
expect(@report).to have_key("run_list")
- expect(@report["run_list"]).to eq(Chef::JSONCompat.to_json(@run_status.node.run_list))
+ expect(@report["run_list"]).to eq(Chef::JSONCompat.to_json(run_status.node.run_list))
end
it "includes the expanded_run_list" do
@@ -457,15 +486,12 @@ describe Chef::ResourceReporter do
it "includes the end_time" do
expect(@report).to have_key("end_time")
- expect(@report["end_time"]).to eq(@run_status.end_time.to_s)
+ expect(@report["end_time"]).to eq(run_status.end_time.to_s)
end
end
context "when the resource is a File" do
- let(:new_resource) { @new_resource }
- let(:current_resource) { @current_resource }
-
it_should_behave_like "a successful client run"
end
@@ -473,8 +499,8 @@ describe Chef::ResourceReporter do
let(:new_resource) do
resource = Chef::Resource::RegistryKey.new('Wubba\Lubba\Dub\Dubs')
resource.values([ { name: "rick", type: :binary, data: 255.chr * 1 } ])
- allow(resource).to receive(:cookbook_name).and_return(@cookbook_name)
- allow(resource).to receive(:cookbook_version).and_return(@cookbook_version)
+ allow(resource).to receive(:cookbook_name).and_return(cookbook_name)
+ allow(resource).to receive(:cookbook_version).and_return(cookbook_version)
resource
end
@@ -491,15 +517,15 @@ describe Chef::ResourceReporter do
before do
@backtrace = ["foo.rb:1 in `foo!'", "bar.rb:2 in `bar!", "'baz.rb:3 in `baz!'"]
- @node = Chef::Node.new
- @node.name("spitfire")
+ node = Chef::Node.new
+ node.name("spitfire")
@exception = ArgumentError.new
allow(@exception).to receive(:inspect).and_return("Net::HTTPClientException")
allow(@exception).to receive(:message).and_return("Object not found")
allow(@exception).to receive(:backtrace).and_return(@backtrace)
- @resource_reporter.run_list_expand_failed(@node, @exception)
- @resource_reporter.run_failed(@exception)
- @report = @resource_reporter.prepare_run_data
+ resource_reporter.run_list_expand_failed(node, @exception)
+ resource_reporter.run_failed(@exception)
+ @report = resource_reporter.prepare_run_data
end
it "includes the exception type in the event data" do
@@ -530,29 +556,29 @@ describe Chef::ResourceReporter do
@bad_resource = Chef::Resource::File.new("/tmp/a-file.txt")
@bad_resource.cookbook_name = nil
- @resource_reporter.resource_action_start(@bad_resource, :create)
- @resource_reporter.resource_current_state_loaded(@bad_resource, :create, @current_resource)
- @resource_reporter.resource_updated(@bad_resource, :create)
- @resource_reporter.resource_completed(@bad_resource)
- @run_status.stop_clock
- @report = @resource_reporter.prepare_run_data
+ events.resource_action_start(@bad_resource, :create)
+ events.resource_current_state_loaded(@bad_resource, :create, current_resource)
+ events.resource_updated(@bad_resource, :create)
+ events.resource_completed(@bad_resource)
+ run_status.stop_clock
+ @report = resource_reporter.prepare_run_data
@first_update_report = @report["resources"].first
end
it "includes an updated resource's initial state" do
- expect(@first_update_report["before"]).to eq(@current_resource.state_for_resource_reporter)
+ expect(@first_update_report["before"]).to eq(current_resource.state_for_resource_reporter)
end
it "includes an updated resource's final state" do
- expect(@first_update_report["after"]).to eq(@new_resource.state_for_resource_reporter)
+ expect(@first_update_report["after"]).to eq(new_resource.state_for_resource_reporter)
end
it "includes the resource's name" do
- expect(@first_update_report["name"]).to eq(@new_resource.name)
+ expect(@first_update_report["name"]).to eq(new_resource.name)
end
it "includes the resource's id property" do
- expect(@first_update_report["id"]).to eq(@new_resource.identity)
+ expect(@first_update_report["id"]).to eq(new_resource.identity)
end
it "includes the elapsed time for the resource to converge" do
@@ -578,17 +604,17 @@ describe Chef::ResourceReporter do
context "when including a resource that overrides Resource#state" do
before do
- @current_state_resource = Chef::Resource::WithState.new("Stateful", @run_context)
+ @current_state_resource = Chef::Resource::WithState.new("Stateful", run_context)
@current_state_resource.state = nil
- @new_state_resource = Chef::Resource::WithState.new("Stateful", @run_context)
+ @new_state_resource = Chef::Resource::WithState.new("Stateful", run_context)
@new_state_resource.state = "Running"
- @resource_reporter.resource_action_start(@new_state_resource, :create)
- @resource_reporter.resource_current_state_loaded(@new_state_resource, :create, @current_state_resource)
- @resource_reporter.resource_updated(@new_state_resource, :create)
- @resource_reporter.resource_completed(@new_state_resource)
- @run_status.stop_clock
- @report = @resource_reporter.prepare_run_data
+ events.resource_action_start(@new_state_resource, :create)
+ events.resource_current_state_loaded(@new_state_resource, :create, @current_state_resource)
+ events.resource_updated(@new_state_resource, :create)
+ events.resource_completed(@new_state_resource)
+ run_status.stop_clock
+ @report = resource_reporter.prepare_run_data
@first_update_report = @report["resources"].first
end
@@ -607,8 +633,8 @@ describe Chef::ResourceReporter do
describe "when updating resource history on the server" do
before do
- @resource_reporter.run_started(@run_status)
- @run_status.start_clock
+ resource_reporter.run_started(run_status)
+ run_status.start_clock
end
context "when the server does not support storing resource history" do
@@ -616,27 +642,27 @@ describe Chef::ResourceReporter do
# 404 getting the run_id
@response = Net::HTTPNotFound.new("a response body", "404", "Not Found")
@error = Net::HTTPClientException.new("404 message", @response)
- expect(@rest_client).to receive(:post)
+ expect(rest_client).to receive(:post)
.with("reports/nodes/spitfire/runs", { action: :start, run_id: @run_id,
- start_time: @start_time.to_s },
+ start_time: start_time.to_s },
{ "X-Ops-Reporting-Protocol-Version" => Chef::ResourceReporter::PROTOCOL_VERSION })
.and_raise(@error)
end
it "assumes the feature is not enabled" do
- @resource_reporter.run_started(@run_status)
- expect(@resource_reporter.reporting_enabled?).to be_falsey
+ resource_reporter.run_started(run_status)
+ expect(resource_reporter.send(:reporting_enabled?)).to be_falsey
end
it "does not send a resource report to the server" do
- @resource_reporter.run_started(@run_status)
- expect(@rest_client).not_to receive(:post)
- @resource_reporter.run_completed(@node)
+ resource_reporter.run_started(run_status)
+ expect(rest_client).not_to receive(:post)
+ resource_reporter.run_completed(node)
end
it "prints an error about the 404" do
expect(Chef::Log).to receive(:trace).with(/404/)
- @resource_reporter.run_started(@run_status)
+ resource_reporter.run_started(run_status)
end
end
@@ -646,26 +672,26 @@ describe Chef::ResourceReporter do
# 500 getting the run_id
@response = Net::HTTPInternalServerError.new("a response body", "500", "Internal Server Error")
@error = Net::HTTPClientException.new("500 message", @response)
- expect(@rest_client).to receive(:post)
- .with("reports/nodes/spitfire/runs", { action: :start, run_id: @run_id, start_time: @start_time.to_s },
+ expect(rest_client).to receive(:post)
+ .with("reports/nodes/spitfire/runs", { action: :start, run_id: @run_id, start_time: start_time.to_s },
{ "X-Ops-Reporting-Protocol-Version" => Chef::ResourceReporter::PROTOCOL_VERSION })
.and_raise(@error)
end
it "assumes the feature is not enabled" do
- @resource_reporter.run_started(@run_status)
- expect(@resource_reporter.reporting_enabled?).to be_falsey
+ resource_reporter.run_started(run_status)
+ expect(resource_reporter.send(:reporting_enabled?)).to be_falsey
end
it "does not send a resource report to the server" do
- @resource_reporter.run_started(@run_status)
- expect(@rest_client).not_to receive(:post)
- @resource_reporter.run_completed(@node)
+ resource_reporter.run_started(run_status)
+ expect(rest_client).not_to receive(:post)
+ resource_reporter.run_completed(node)
end
it "prints an error about the error" do
expect(Chef::Log).to receive(:info).with(/500/)
- @resource_reporter.run_started(@run_status)
+ resource_reporter.run_started(run_status)
end
end
@@ -676,8 +702,8 @@ describe Chef::ResourceReporter do
# 500 getting the run_id
@response = Net::HTTPInternalServerError.new("a response body", "500", "Internal Server Error")
@error = Net::HTTPClientException.new("500 message", @response)
- expect(@rest_client).to receive(:post)
- .with("reports/nodes/spitfire/runs", { action: :start, run_id: @run_id, start_time: @start_time.to_s },
+ expect(rest_client).to receive(:post)
+ .with("reports/nodes/spitfire/runs", { action: :start, run_id: @run_id, start_time: start_time.to_s },
{ "X-Ops-Reporting-Protocol-Version" => Chef::ResourceReporter::PROTOCOL_VERSION })
.and_raise(@error)
end
@@ -689,7 +715,7 @@ describe Chef::ResourceReporter do
it "fails the run and prints an message about the error" do
expect(Chef::Log).to receive(:error).with(/500/)
expect do
- @resource_reporter.run_started(@run_status)
+ resource_reporter.run_started(run_status)
end.to raise_error(Net::HTTPClientException)
end
end
@@ -697,29 +723,29 @@ describe Chef::ResourceReporter do
context "after creating the run history document" do
before do
response = { "uri" => "https://example.com/reports/nodes/spitfire/runs/@run_id" }
- expect(@rest_client).to receive(:post)
- .with("reports/nodes/spitfire/runs", { action: :start, run_id: @run_id, start_time: @start_time.to_s },
+ expect(rest_client).to receive(:post)
+ .with("reports/nodes/spitfire/runs", { action: :start, run_id: @run_id, start_time: start_time.to_s },
{ "X-Ops-Reporting-Protocol-Version" => Chef::ResourceReporter::PROTOCOL_VERSION })
.and_return(response)
- @resource_reporter.run_started(@run_status)
+ resource_reporter.run_started(run_status)
end
it "creates a run document on the server at the start of the run" do
- expect(@resource_reporter.run_id).to eq(@run_id)
+ expect(resource_reporter.run_id).to eq(@run_id)
end
it "updates the run document with resource updates at the end of the run" do
# update some resources...
- @resource_reporter.resource_action_start(@new_resource, :create)
- @resource_reporter.resource_current_state_loaded(@new_resource, :create, @current_resource)
- @resource_reporter.resource_updated(@new_resource, :create)
+ events.resource_action_start(new_resource, :create)
+ events.resource_current_state_loaded(new_resource, :create, current_resource)
+ events.resource_updated(new_resource, :create)
- allow(@resource_reporter).to receive(:end_time).and_return(@end_time)
- @expected_data = @resource_reporter.prepare_run_data
+ allow(resource_reporter).to receive(:end_time).and_return(end_time)
+ @expected_data = resource_reporter.prepare_run_data
response = { "result" => "ok" }
- expect(@rest_client).to receive(:raw_request).ordered do |method, url, headers, data|
+ expect(rest_client).to receive(:raw_request).ordered do |method, url, headers, data|
expect(method).to eq(:POST)
expect(headers).to eq({ "Content-Encoding" => "gzip",
"X-Ops-Reporting-Protocol-Version" => Chef::ResourceReporter::PROTOCOL_VERSION,
@@ -730,7 +756,7 @@ describe Chef::ResourceReporter do
response
end
- @resource_reporter.run_completed(@node)
+ resource_reporter.run_completed(node)
end
end
@@ -747,34 +773,34 @@ describe Chef::ResourceReporter do
it "should log 4xx errors" do
response = Net::HTTPClientError.new("forbidden", "403", "Forbidden")
error = Net::HTTPClientException.new("403 message", response)
- allow(@rest_client).to receive(:raw_request).and_raise(error)
+ allow(rest_client).to receive(:raw_request).and_raise(error)
expect(Chef::Log).to receive(:error).with(/403/)
- @resource_reporter.post_reporting_data
+ resource_reporter.post_reporting_data
end
it "should log error 5xx errors" do
response = Net::HTTPServerError.new("internal error", "500", "Internal Server Error")
error = Net::HTTPFatalError.new("500 message", response)
- allow(@rest_client).to receive(:raw_request).and_raise(error)
+ allow(rest_client).to receive(:raw_request).and_raise(error)
expect(Chef::Log).to receive(:error).with(/500/)
- @resource_reporter.post_reporting_data
+ resource_reporter.post_reporting_data
end
it "should log if a socket error happens" do
- allow(@rest_client).to receive(:raw_request).and_raise(SocketError.new("test socket error"))
+ allow(rest_client).to receive(:raw_request).and_raise(SocketError.new("test socket error"))
expect(Chef::Log).to receive(:error).with(/test socket error/)
- @resource_reporter.post_reporting_data
+ resource_reporter.post_reporting_data
end
it "should raise if an unkwown error happens" do
- allow(@rest_client).to receive(:raw_request).and_raise(Exception.new)
+ allow(rest_client).to receive(:raw_request).and_raise(Exception.new)
expect do
- @resource_reporter.post_reporting_data
+ resource_reporter.post_reporting_data
end.to raise_error(Exception)
end
end