File Manager
Viewing File: data_collector_spec.rb
#
# 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 "spec_helper"
require "chef/data_collector"
require "chef/resource_builder"
describe Chef::DataCollector do
describe ".register_reporter?" do
context "when no data collector URL is configured" do
it "returns false" do
Chef::Config[:data_collector][:server_url] = 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" }
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
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
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
let(:token) { nil }
context "when report is enabled for current mode" do
before do
allow(Chef::DataCollector).to receive(:reporter_enabled_for_current_mode?).and_return(true)
end
context "when the current mode is solo" do
before do
Chef::Config[:solo] = true
end
it "returns true" do
expect(Chef::DataCollector.register_reporter?).to be(true)
end
end
context "when the current mode is local mode" do
before do
Chef::Config[:local_mode] = true
end
it "returns false" do
expect(Chef::DataCollector.register_reporter?).to be(true)
end
end
context "when the current mode is client mode" do
before do
Chef::Config[:local_mode] = false
Chef::Config[:solo] = false
end
it "returns true" do
expect(Chef::DataCollector.register_reporter?).to be_truthy
end
end
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
end
end
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
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
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
end
end
context "when running in local mode" do
before do
Chef::Config[:solo] = false
Chef::Config[:local_mode] = 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
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
end
end
context "when running in client mode" do
before do
Chef::Config[:solo] = false
Chef::Config[:local_mode] = false
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
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
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
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
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") }
before do
Chef::Config[:client_key] = client_key
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
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
it "updates the run status" do
expect(reporter).to receive(:update_run_status).with(run_status)
reporter.run_started(run_status)
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)
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)
end
before do
reporter.send(:update_run_status, run_status)
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
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 "#converge_complete" do
it "detects and processes any unprocessed resources" do
expect(reporter).to receive(:detect_unprocessed_resources)
reporter.converge_complete
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
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") }
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
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
end
describe "#resource_up_to_date" do
let(:new_resource) { double("new_resource") }
let(:action) { double("action") }
let(:resource_report) { double("resource_report") }
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
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
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") }
before do
allow(reporter).to receive(:nested_resource?)
allow(resource_report).to receive(:skipped)
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
end
describe "#resource_updated" do
let(:resource_report) { double("resource_report") }
before do
allow(reporter).to receive(:current_resource_report).and_return(resource_report)
allow(resource_report).to receive(:updated)
end
it "marks the resource report as updated" do
expect(resource_report).to receive(:updated)
reporter.resource_updated("new_resource", "action")
end
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") }
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)
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
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)
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)
end
end
end
describe "#resource_completed" do
let(:new_resource) { double("new_resource") }
let(:resource_report) { double("resource_report") }
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
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)
end
end
context "when there is a current resource report" do
before do
allow(reporter).to receive(:current_resource_report).and_return(resource_report)
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)
end
end
context "when the resource is not a nested resource" do
before do
allow(reporter).to receive(:nested_resource?).with(new_resource).and_return(false)
end
it "marks the current resource report as finished" do
expect(resource_report).to receive(:finish)
reporter.resource_completed(new_resource)
end
it "nils out the current resource report" do
expect(reporter).to receive(:clear_current_resource_report)
reporter.resource_completed(new_resource)
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 "#run_list_expand_failed" do
let(:node) { double("node") }
let(:error_mapper) { double("error_mapper") }
let(:exception) { double("exception") }
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
describe "#cookbook_resolution_failed" do
let(:error_mapper) { double("error_mapper") }
let(:exception) { double("exception") }
let(:expanded_run_list) { double("expanded_run_list") }
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
end
describe "#cookbook_sync_failed" do
let(:cookbooks) { double("cookbooks") }
let(:error_mapper) { double("error_mapper") }
let(:exception) { double("exception") }
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
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
it "does not raise an exception" do
expect { reporter.send(:disable_reporter_on_error) { true } }.not_to raise_error
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
[ 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") }
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
end
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
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)
end
end
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)
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
end
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
end
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")
resource_a.action = [ :enable, :start ]
resource_b.action = :start
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)
allow(reporter).to receive(:run_context).and_return(run_context)
# 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)
# 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)
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
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)
allow(reporter).to receive(:run_context).and_return(run_context)
# 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" }
# process the actions for resource_a, but not resource_b
reporter.resource_up_to_date(resource_a, :start)
reporter.resource_completed(resource_a)
expect { reporter.send(:detect_unprocessed_resources) }.not_to raise_error
end
end
end
end