diff options
author | Noah Kantrowitz <noah@coderanger.net> | 2016-08-02 16:36:05 -0700 |
---|---|---|
committer | Noah Kantrowitz <noah@coderanger.net> | 2016-08-02 16:36:05 -0700 |
commit | 0368df838c36fd5a54c75007aae3c2e28cbdba1b (patch) | |
tree | d89eac235ed3890c4628c59505299627a44508ca /lib | |
parent | 16fbf0a9a81daa1e3418eca251e59b4545ae0b88 (diff) | |
parent | 767a45530b373bbd4818b93ab1efe5cd3c7da5ed (diff) | |
download | chef-0368df838c36fd5a54c75007aae3c2e28cbdba1b.tar.gz |
Merge branch 'master' into configoption
Diffstat (limited to 'lib')
60 files changed, 1057 insertions, 449 deletions
diff --git a/lib/chef/application.rb b/lib/chef/application.rb index fa740e068a..c377ffdb36 100644 --- a/lib/chef/application.rb +++ b/lib/chef/application.rb @@ -29,6 +29,10 @@ require "tmpdir" require "rbconfig" require "chef/application/exit_code" require "yaml" +require "resolv" +# on linux, we replace the glibc resolver with the ruby resolv library, which +# supports reloading. +require "resolv-replace" if RbConfig::CONFIG["host_os"] =~ /linux/ class Chef class Application diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb index cec47ac071..71bb300971 100644 --- a/lib/chef/application/client.rb +++ b/lib/chef/application/client.rb @@ -27,6 +27,7 @@ require "chef/handler/error_report" require "chef/workstation_config_loader" require "chef/mixin/shell_out" require "chef-config/mixin/dot_d" +require "mixlib/archive" class Chef::Application::Client < Chef::Application include Chef::Mixin::ShellOut @@ -342,8 +343,7 @@ class Chef::Application::Client < Chef::Application FileUtils.mkdir_p(Chef::Config.chef_repo_path) tarball_path = File.join(Chef::Config.chef_repo_path, "recipes.tgz") fetch_recipe_tarball(Chef::Config[:recipe_url], tarball_path) - result = shell_out!("tar zxvf #{tarball_path} -C #{Chef::Config.chef_repo_path}") - Chef::Log.debug "#{result.stdout}" + Mixlib::Archive.new(tarball_path).extract(Chef::Config.chef_repo_path, perms: false, ignore: /^\.$/) end end diff --git a/lib/chef/application/solo.rb b/lib/chef/application/solo.rb index d38e97e82b..8bf381a975 100644 --- a/lib/chef/application/solo.rb +++ b/lib/chef/application/solo.rb @@ -29,6 +29,7 @@ require "fileutils" require "chef/mixin/shell_out" require "pathname" require "chef-config/mixin/dot_d" +require "mixlib/archive" class Chef::Application::Solo < Chef::Application include Chef::Mixin::ShellOut @@ -249,6 +250,13 @@ class Chef::Application::Solo < Chef::Application ARGV[dash_r] = "--recipe-url" end + # For back compat reasons, we need to ensure that we try and use the cache_path as a repo first + Chef::Log.debug "Current chef_repo_path is #{Chef::Config.chef_repo_path}" + + if !Chef::Config.has_key?(:cookbook_path) && !Chef::Config.has_key?(:chef_repo_path) + Chef::Config.chef_repo_path = Chef::Config.find_chef_repo_path(Chef::Config[:cache_path]) + end + Chef::Config[:local_mode] = true else configure_legacy_mode! @@ -274,8 +282,7 @@ class Chef::Application::Solo < Chef::Application FileUtils.mkdir_p(recipes_path) tarball_path = File.join(recipes_path, "recipes.tgz") fetch_recipe_tarball(Chef::Config[:recipe_url], tarball_path) - result = shell_out!("tar zxvf #{tarball_path} -C #{recipes_path}") - Chef::Log.debug "#{result.stdout}" + Mixlib::Archive.new(tarball_path).extract(Chef::Config.chef_repo_path, perms: false, ignore: /^\.$/) end # json_attribs shuld be fetched after recipe_url tarball is unpacked. diff --git a/lib/chef/audit/audit_reporter.rb b/lib/chef/audit/audit_reporter.rb index a40cae93dd..8546a21bb4 100644 --- a/lib/chef/audit/audit_reporter.rb +++ b/lib/chef/audit/audit_reporter.rb @@ -140,7 +140,11 @@ class Chef # Save the audit report to local disk error_file = "failed-audit-data.json" Chef::FileCache.store(error_file, Chef::JSONCompat.to_json_pretty(run_data), 0640) - Chef::Log.error("Failed to post audit report to server. Saving report to #{Chef::FileCache.load(error_file, false)}") + if Chef::Config.chef_zero.enabled + Chef::Log.debug("Saving audit report to #{Chef::FileCache.load(error_file, false)}") + else + Chef::Log.error("Failed to post audit report to server. Saving report to #{Chef::FileCache.load(error_file, false)}") + end end else Chef::Log.error("Failed to post audit report to server (#{e})") diff --git a/lib/chef/audit/runner.rb b/lib/chef/audit/runner.rb index 100a72d2e1..837346381c 100644 --- a/lib/chef/audit/runner.rb +++ b/lib/chef/audit/runner.rb @@ -165,7 +165,7 @@ class Chef add_example_group_methods run_context.audits.each do |name, group| ctl_grp = RSpec::Core::ExampleGroup.__control_group__(*group.args, &group.block) - RSpec.world.register(ctl_grp) + RSpec.world.record(ctl_grp) end end diff --git a/lib/chef/chef_fs/chef_fs_data_store.rb b/lib/chef/chef_fs/chef_fs_data_store.rb index aa5a6d5a69..6b3e830f8d 100644 --- a/lib/chef/chef_fs/chef_fs_data_store.rb +++ b/lib/chef/chef_fs/chef_fs_data_store.rb @@ -458,6 +458,7 @@ class Chef # We want to delete just the ones that == POLICY next unless policy.name.rpartition("-")[0] == path[1] policy.delete(false) + FileSystemCache.instance.delete!(policy.file_path) found_policy = true end raise ChefZero::DataStore::DataNotFoundError.new(path) if !found_policy diff --git a/lib/chef/chef_fs/file_system/multiplexed_dir.rb b/lib/chef/chef_fs/file_system/multiplexed_dir.rb index d3951edd51..21abc012f8 100644 --- a/lib/chef/chef_fs/file_system/multiplexed_dir.rb +++ b/lib/chef/chef_fs/file_system/multiplexed_dir.rb @@ -41,7 +41,7 @@ class Chef child_entry = dir.child(name) if child_entry.exists? if result - Chef::Log.debug("Child with name '#{child_entry.name}' found in multiple directories: #{result.parent.path_for_printing} and #{child_entry.parent.path_for_printing}") unless seen[child.name].path_for_printing == child.path_for_printing + Chef::Log.debug("Child with name '#{child_entry.name}' found in multiple directories: #{result.parent.path_for_printing} and #{child_entry.parent.path_for_printing}") else result = child_entry end diff --git a/lib/chef/chef_fs/file_system/repository/acls_dir.rb b/lib/chef/chef_fs/file_system/repository/acls_dir.rb index 619031aa70..110befdf22 100644 --- a/lib/chef/chef_fs/file_system/repository/acls_dir.rb +++ b/lib/chef/chef_fs/file_system/repository/acls_dir.rb @@ -28,14 +28,16 @@ class Chef module Repository class AclsDir < Repository::Directory + BARE_FILES = %w{ organization.json root } + def can_have_child?(name, is_dir) - is_dir ? Chef::ChefFS::FileSystem::ChefServer::AclsDir::ENTITY_TYPES.include?(name) : name == "organization.json" + is_dir ? Chef::ChefFS::FileSystem::ChefServer::AclsDir::ENTITY_TYPES.include?(name) : BARE_FILES.include?(name) end protected def make_child_entry(child_name) - if child_name == "organization.json" + if BARE_FILES.include? child_name Acl.new(child_name, self) else AclsSubDir.new(child_name, self) diff --git a/lib/chef/chef_fs/file_system/repository/base_file.rb b/lib/chef/chef_fs/file_system/repository/base_file.rb index a768bcf971..3e1edc8d62 100644 --- a/lib/chef/chef_fs/file_system/repository/base_file.rb +++ b/lib/chef/chef_fs/file_system/repository/base_file.rb @@ -16,6 +16,8 @@ # limitations under the License. # +require "chef/chef_fs/file_system_cache" + class Chef module ChefFS module FileSystem @@ -99,6 +101,7 @@ class Chef end def delete(_) + FileSystemCache.instance.delete!(file_path) File.delete(file_path) rescue Errno::ENOENT raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) diff --git a/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_cookbook_entry.rb b/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_cookbook_entry.rb index 9d1538e46e..4019c6985b 100644 --- a/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_cookbook_entry.rb +++ b/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_cookbook_entry.rb @@ -120,6 +120,7 @@ class Chef end def delete(recurse) + FileSystemCache.instance.delete!(file_path) begin if dir? if !recurse diff --git a/lib/chef/chef_fs/file_system/repository/directory.rb b/lib/chef/chef_fs/file_system/repository/directory.rb index dae467993a..328cf92b03 100644 --- a/lib/chef/chef_fs/file_system/repository/directory.rb +++ b/lib/chef/chef_fs/file_system/repository/directory.rb @@ -16,6 +16,8 @@ # limitations under the License. # +require "chef/chef_fs/file_system_cache" + class Chef module ChefFS module FileSystem @@ -68,9 +70,11 @@ class Chef end def children - dir_ls.sort. + return FileSystemCache.instance.children(file_path) if FileSystemCache.instance.exist?(file_path) + children = dir_ls.sort. map { |child_name| make_child_entry(child_name) }. select { |new_child| new_child.fs_entry_valid? && can_have_child?(new_child.name, new_child.dir?) } + FileSystemCache.instance.set_children(file_path, children) rescue Errno::ENOENT => e raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) end @@ -80,6 +84,7 @@ class Chef if child.exists? raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, child) end + FileSystemCache.instance.delete!(child.file_path) if file_contents child.write(file_contents) else @@ -118,6 +123,7 @@ class Chef raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, self) end begin + FileSystemCache.instance.delete!(file_path) Dir.mkdir(file_path) rescue Errno::EEXIST raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, self) @@ -134,6 +140,7 @@ class Chef raise MustDeleteRecursivelyError.new(self, $!) end FileUtils.rm_r(file_path) + FileSystemCache.instance.delete!(file_path) else raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) end @@ -145,6 +152,10 @@ class Chef protected + def write(data) + raise FileSystemError.new(self, nil, "attempted to write to a directory entry") + end + def make_child_entry(child_name) raise "Not Implemented" end diff --git a/lib/chef/chef_fs/file_system_cache.rb b/lib/chef/chef_fs/file_system_cache.rb new file mode 100644 index 0000000000..a9d8d8bfe4 --- /dev/null +++ b/lib/chef/chef_fs/file_system_cache.rb @@ -0,0 +1,80 @@ +# +# Copyright:: Copyright 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 "singleton" +require "chef/client" + +class Chef + module ChefFS + class FileSystemCache + include Singleton + + def initialize + @cache = {} + + Chef::Client.when_run_starts do + FileSystemCache.instance.reset! + end + end + + def reset! + @cache = {} + end + + def exist?(path) + @cache.key?(path) + end + + def children(path) + @cache[path]["children"] + end + + def set_children(path, val) + @cache[path] ||= { "children" => [] } + @cache[path]["children"] = val + val + end + + def delete!(path) + parent = _get_parent(path) + Chef::Log.debug("Deleting parent #{parent} and #{path} from FileSystemCache") + if @cache.key?(path) + @cache.delete(path) + end + if !parent.nil? && @cache.key?(parent) + @cache.delete(parent) + end + end + + def fetch(path) + if @cache.key?(path) + @cache[path] + else + false + end + end + + private + + def _get_parent(path) + parts = ChefFS::PathUtils.split(path) + return nil if parts.nil? || parts.length < 2 + ChefFS::PathUtils.join(*parts[0..-2]) + end + end + end +end diff --git a/lib/chef/data_collector.rb b/lib/chef/data_collector.rb index c8dd354f60..dbb0b3771a 100644 --- a/lib/chef/data_collector.rb +++ b/lib/chef/data_collector.rb @@ -22,6 +22,7 @@ require "uri" require "chef/event_dispatch/base" require "chef/data_collector/messages" require "chef/data_collector/resource_report" +require "ostruct" class Chef @@ -51,12 +52,14 @@ class Chef # and exports its data through a webhook-like mechanism to a configured # endpoint. class Reporter < EventDispatch::Base - attr_reader :completed_resources, :status, :exception, :error_descriptions, - :expanded_run_list, :run_status, :http, + attr_reader :all_resource_reports, :status, :exception, :error_descriptions, + :expanded_run_list, :run_context, :run_status, :http, :current_resource_report, :enabled def initialize - @completed_resources = [] + validate_data_collector_server_url! + + @all_resource_reports = [] @current_resource_loaded = nil @error_descriptions = {} @expanded_run_list = {} @@ -93,6 +96,29 @@ class Chef 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 @@ -100,13 +126,7 @@ class Chef # 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) - update_current_resource_report( - Chef::DataCollector::ResourceReport.new( - new_resource, - action, - current_resource - ) - ) + update_current_resource_report(create_resource_report(new_resource, action, current_resource)) end # see EventDispatch::Base#resource_up_to_date @@ -116,19 +136,15 @@ class Chef 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 + # 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) - update_current_resource_report( - Chef::DataCollector::ResourceReport.new( - new_resource, - action - ) - ) - current_resource_report.skipped(conditional) + resource_report = create_resource_report(new_resource, action) + resource_report.skipped(conditional) + update_current_resource_report(resource_report) end # see EventDispatch::Base#resource_updated @@ -154,13 +170,12 @@ class Chef end # see EventDispatch::Base#resource_completed - # Mark the ResourceReport instance as finished (for timing details) - # and add it to the list of resources encountered during this run. + # 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_completed_resource(current_resource_report) + add_resource_report(current_resource_report) update_current_resource_report(nil) end end @@ -223,7 +238,8 @@ class Chef yield rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError, Net::HTTPBadResponse, - Net::HTTPHeaderSyntaxError, Net::ProtocolError, OpenSSL::SSL::SSLError => e + Net::HTTPHeaderSyntaxError, Net::ProtocolError, OpenSSL::SSL::SSLError, + Errno::EHOSTDOWN => e disable_data_collector_reporter code = if e.respond_to?(:response) && e.response.code e.response.code.to_s @@ -266,12 +282,11 @@ class Chef # we have nothing to report. return unless run_status - send_to_data_collector(Chef::DataCollector::Messages.node_update_message(run_status).to_json) send_to_data_collector( Chef::DataCollector::Messages.run_end_message( run_status: run_status, expanded_run_list: expanded_run_list, - completed_resources: completed_resources, + resources: all_resource_reports, status: opts[:status], error_descriptions: error_descriptions ).to_json @@ -297,8 +312,12 @@ class Chef Chef::Config[:data_collector][:token] end - def add_completed_resource(resource_report) - @completed_resources << resource_report + 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_hash + ) end def disable_data_collector_reporter @@ -321,6 +340,38 @@ class Chef @error_descriptions = discription_hash end + def create_resource_report(new_resource, action, current_resource = nil) + Chef::DataCollector::ResourceReport.new( + new_resource, + action, + current_resource + ) + end + + def detect_unprocessed_resources + # create a Set containing all resource+action combinations from + # the Resource Collection + collection_resources = Set.new + run_context.resource_collection.all_resources.each do |resource| + Array(resource.action).each do |action| + collection_resources.add([resource, action]) + end + end + + # Delete from the Set any resource+action combination we have + # already processed. + all_resource_reports.each do |report| + collection_resources.delete([report.resource, report.action]) + end + + # The items remaining in the Set are unprocessed resource+actions, + # so we'll create new resource reports for them which default to + # a state of "unprocessed". + collection_resources.each do |resource, action| + add_resource_report(create_resource_report(resource, action)) + 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 @@ -328,6 +379,20 @@ class Chef def nested_resource?(new_resource) @current_resource_report && @current_resource_report.new_resource != new_resource end + + def validate_data_collector_server_url! + raise Chef::Exceptions::ConfigurationError, + "Chef::Config[:data_collector][:server_url] is empty. Please supply a valid URL." if data_collector_server_url.empty? + + begin + uri = URI(data_collector_server_url) + rescue URI::InvalidURIError + raise Chef::Exceptions::ConfigurationError, "Chef::Config[:data_collector][:server_url] (#{data_collector_server_url}) is not a valid URI." + end + + 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." if uri.host.nil? + end end end end diff --git a/lib/chef/data_collector/messages.rb b/lib/chef/data_collector/messages.rb index e23262c9e4..8c2a84b580 100644 --- a/lib/chef/data_collector/messages.rb +++ b/lib/chef/data_collector/messages.rb @@ -68,17 +68,18 @@ class Chef "id" => run_status.run_id, "message_version" => "1.0.0", "message_type" => "run_converge", + "node" => run_status.node, "node_name" => run_status.node.name, "organization_name" => organization, - "resources" => reporter_data[:completed_resources].map(&:for_json), + "resources" => reporter_data[:resources].map(&:report_data), "run_id" => run_status.run_id, "run_list" => run_status.node.run_list.for_json, "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[:completed_resources].count, - "updated_resource_count" => reporter_data[:completed_resources].select { |r| r.status == "updated" }.count, + "total_resource_count" => reporter_data[:resources].count, + "updated_resource_count" => reporter_data[:resources].select { |r| r.report_data["status"] == "updated" }.count, } message["error"] = { @@ -90,36 +91,6 @@ class Chef message end - - # - # Message payload that is sent to the DataCollector server at the - # end of a Chef run. - # - # @param run_status [Chef::RunStatus] The RunStatus instance for this node/run. - # - # @return [Hash] A hash containing the node object and related metadata. - # - def self.node_update_message(run_status) - { - "entity_name" => run_status.node.name, - "entity_type" => "node", - "entity_uuid" => node_uuid, - "id" => SecureRandom.uuid, - "message_version" => "1.1.0", - "message_type" => "action", - "organization_name" => organization, - "recorded_at" => Time.now.utc.iso8601, - "remote_hostname" => run_status.node["fqdn"], - "requestor_name" => run_status.node.name, - "requestor_type" => "client", - "run_id" => run_status.run_id, - "service_hostname" => chef_server_fqdn(run_status), - "source" => collector_source, - "task" => "update", - "user_agent" => Chef::HTTP::HTTPRequest::DEFAULT_UA, - "data" => run_status.node, - } - end end end end diff --git a/lib/chef/data_collector/messages/helpers.rb b/lib/chef/data_collector/messages/helpers.rb index 3e52f80047..c0c700f847 100644 --- a/lib/chef/data_collector/messages/helpers.rb +++ b/lib/chef/data_collector/messages/helpers.rb @@ -148,8 +148,8 @@ class Chef end def update_metadata(key, value) - metadata[key] = value - Chef::FileCache.store(metadata_filename, metadata.to_json, 0644) + updated_metadata = metadata.tap { |x| x[key] = value } + Chef::FileCache.store(metadata_filename, updated_metadata.to_json, 0644) end def metadata_filename diff --git a/lib/chef/data_collector/resource_report.rb b/lib/chef/data_collector/resource_report.rb index 1793fe2c9d..dcaf9c8e44 100644 --- a/lib/chef/data_collector/resource_report.rb +++ b/lib/chef/data_collector/resource_report.rb @@ -22,13 +22,14 @@ class Chef class DataCollector class ResourceReport - attr_reader :action, :current_resource, :elapsed_time, :new_resource, :status - attr_accessor :conditional, :exception + 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) @@ -54,22 +55,32 @@ class Chef @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_hash hash = { - "type" => new_resource.resource_name.to_sym, - "name" => new_resource.name.to_s, - "id" => new_resource.identity.to_s, - "after" => new_resource.state_for_resource_reporter, - "before" => current_resource ? current_resource.state_for_resource_reporter : {}, - "duration" => (elapsed_time * 1000).to_i.to_s, - "delta" => new_resource.respond_to?(:diff) ? new_resource.diff : "", - "result" => action.to_s, - "status" => status, + "type" => new_resource.resource_name.to_sym, + "name" => new_resource.name.to_s, + "id" => new_resource.identity.to_s, + "after" => new_resource.state_for_resource_reporter, + "before" => current_resource ? current_resource.state_for_resource_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" diff --git a/lib/chef/decorator/unchain.rb b/lib/chef/decorator/unchain.rb new file mode 100644 index 0000000000..8093c70f4c --- /dev/null +++ b/lib/chef/decorator/unchain.rb @@ -0,0 +1,59 @@ +class Chef + class Decorator < SimpleDelegator + # + # This decorator unchains method call chains and turns them into method calls + # with variable args. So this: + # + # node.set_unless["foo"]["bar"] = "baz" + # + # Can become: + # + # node.set_unless("foo", "bar", "baz") + # + # While this is a decorator it is not a Decorator and does not inherit because + # it deliberately does not need or want the method_missing magic. It is not legal + # to call anything on the intermediate values and only supports method chaining with + # #[] until the chain comes to an end with #[]=, so does not behave like a hash or + # array... e.g. + # + # node.default['foo'].keys is legal + # node.set_unless['foo'].keys is not legal now or ever + # + class Unchain + attr_accessor :__path__ + attr_accessor :__method__ + + def initialize(obj, method) + @__path__ = [] + @__method__ = method + @delegate_sd_obj = obj + end + + def [](key) + __path__.push(key) + self + end + + def []=(key, value) + __path__.push(key) + @delegate_sd_obj.public_send(__method__, *__path__, value) + end + + # unfortunately we have to support method_missing for node.set_unless.foo.bar = 'baz' notation + def method_missing(symbol, *args) + if symbol == :to_ary + merged_attributes.send(symbol, *args) + elsif args.empty? + Chef.log_deprecation %q{method access to node attributes (node.foo.bar) is deprecated and will be removed in Chef 13, please use bracket syntax (node["foo"]["bar"])} + self[symbol] + elsif symbol.to_s =~ /=$/ + Chef.log_deprecation %q{method setting of node attributes (node.foo="bar") is deprecated and will be removed in Chef 13, please use bracket syntax (node["foo"]="bar")} + key_to_set = symbol.to_s[/^(.+)=$/, 1] + self[key_to_set] = (args.length == 1 ? args[0] : args) + else + raise NoMethodError, "Undefined node attribute or method `#{symbol}' on `node'" + end + end + end + end +end diff --git a/lib/chef/dsl/cheffish.rb b/lib/chef/dsl/cheffish.rb index de052bbe5c..03290b3674 100644 --- a/lib/chef/dsl/cheffish.rb +++ b/lib/chef/dsl/cheffish.rb @@ -27,6 +27,7 @@ class Chef chef_acl chef_client chef_container + chef_data_bag_item chef_data_bag chef_environment chef_group diff --git a/lib/chef/dsl/declare_resource.rb b/lib/chef/dsl/declare_resource.rb index 8d76ddfb31..86227a0f9d 100644 --- a/lib/chef/dsl/declare_resource.rb +++ b/lib/chef/dsl/declare_resource.rb @@ -71,7 +71,15 @@ class Chef # delete_resource!(:template, '/x/y.txy') # def delete_resource!(type, name, run_context: self.run_context) - run_context.resource_collection.delete("#{type}[#{name}]") + run_context.resource_collection.delete("#{type}[#{name}]").tap do |resource| + # Purge any pending notifications too. This will not raise an exception + # if there are no notifications. + if resource + run_context.before_notification_collection.delete(resource.declared_key) + run_context.immediate_notification_collection.delete(resource.declared_key) + run_context.delayed_notification_collection.delete(resource.declared_key) + end + end end # Lookup a resource in the resource collection by name and delete it. Returns diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index ea90d80cd8..43759568a7 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -106,7 +106,12 @@ class Chef # for back compat, need to raise an error that inherits from ArgumentError class CookbookNotFoundInRepo < ArgumentError; end class RecipeNotFound < ArgumentError; end + # AttributeNotFound really means the attribute file could not be found class AttributeNotFound < RuntimeError; end + # NoSuchAttribute is raised on access by node.read!("foo", "bar") when node["foo"]["bar"] does not exist. + class NoSuchAttribute < RuntimeError; end + # AttributeTypeMismatch is raised by node.write!("foo", "bar", "baz") when e.g. node["foo"] = "bar" (overwriting String with Hash) + class AttributeTypeMismatch < RuntimeError; end class MissingCookbookDependency < StandardError; end # CHEF-5120 class InvalidCommandOption < RuntimeError; end class CommandTimeout < RuntimeError; end diff --git a/lib/chef/http.rb b/lib/chef/http.rb index c6afa97d23..3e69f58383 100644 --- a/lib/chef/http.rb +++ b/lib/chef/http.rb @@ -232,11 +232,11 @@ class Chef # PERFORMANCE CRITICAL: *MUST* lazy require here otherwise we load up webrick # via chef-zero and that hits DNS (at *require* time) which may timeout, # when for most knife/chef-client work we never need/want this loaded. - Thread.exclusive { - unless defined?(SocketlessChefZeroClient) - require "chef/http/socketless_chef_zero_client" - end - } + + unless defined?(SocketlessChefZeroClient) + require "chef/http/socketless_chef_zero_client" + end + SocketlessChefZeroClient.new(base_url) else BasicClient.new(base_url, :ssl_policy => Chef::HTTP::APISSLPolicy) diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb index f5dc29371f..ee4d9ce7af 100644 --- a/lib/chef/knife/bootstrap.rb +++ b/lib/chef/knife/bootstrap.rb @@ -101,6 +101,14 @@ class Chef :description => "The proxy server for the node being bootstrapped", :proc => Proc.new { |p| Chef::Config[:knife][:bootstrap_proxy] = p } + option :bootstrap_proxy_user, + :long => "--bootstrap-proxy-user PROXY_USER", + :description => "The proxy authentication username for the node being bootstrapped" + + option :bootstrap_proxy_pass, + :long => "--bootstrap-proxy-pass PROXY_PASS", + :description => "The proxy authentication password for the node being bootstrapped" + option :bootstrap_no_proxy, :long => "--bootstrap-no-proxy [NO_PROXY_URL|NO_PROXY_IP]", :description => "Do not proxy locations for the node being bootstrapped; this option is used internally by Opscode", @@ -224,6 +232,7 @@ class Chef unless valid_values.include?(v) raise "Invalid value '#{v}' for --node-ssl-verify-mode. Valid values are: #{valid_values.join(", ")}" end + v } option :node_verify_api_cert, diff --git a/lib/chef/knife/cookbook_create.rb b/lib/chef/knife/cookbook_create.rb index 950de380f8..ccb78bb7a6 100644 --- a/lib/chef/knife/cookbook_create.rb +++ b/lib/chef/knife/cookbook_create.rb @@ -56,6 +56,10 @@ class Chef :description => "Email address of cookbook maintainer" def run + Chef::Log.deprecation <<EOF +This command is being deprecated in favor of `chef generate cookbook` and will soon return an error. +Please use `chef generate cookbook` instead of this command. +EOF self.config = Chef::Config.merge!(config) if @name_args.length < 1 show_usage diff --git a/lib/chef/knife/cookbook_site_download.rb b/lib/chef/knife/cookbook_site_download.rb index 7d0e21791d..43677cfa78 100644 --- a/lib/chef/knife/cookbook_site_download.rb +++ b/lib/chef/knife/cookbook_site_download.rb @@ -37,6 +37,13 @@ class Chef :long => "--force", :description => "Force download deprecated version" + option :supermarket_site, + :short => "-m SUPERMARKET_SITE", + :long => "--supermarket-site SUPERMARKET_SITE", + :description => "Supermarket Site", + :default => "https://supermarket.chef.io", + :proc => Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } + def run if current_cookbook_deprecated? message = "DEPRECATION: This cookbook has been deprecated. " @@ -59,7 +66,7 @@ class Chef private def cookbooks_api_url - "https://supermarket.chef.io/api/v1/cookbooks" + "#{config[:supermarket_site]}/api/v1/cookbooks" end def current_cookbook_data diff --git a/lib/chef/knife/cookbook_site_install.rb b/lib/chef/knife/cookbook_site_install.rb index 45f3061d87..43d015dcc4 100644 --- a/lib/chef/knife/cookbook_site_install.rb +++ b/lib/chef/knife/cookbook_site_install.rb @@ -19,6 +19,7 @@ require "chef/knife" require "chef/exceptions" require "shellwords" +require "mixlib/archive" class Chef class Knife @@ -59,6 +60,13 @@ class Chef :boolean => true, :default => false + option :supermarket_site, + :short => "-m SUPERMARKET_SITE", + :long => "--supermarket-site SUPERMARKET_SITE", + :description => "Supermarket Site", + :default => "https://supermarket.chef.io", + :proc => Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } + attr_reader :cookbook_name attr_reader :vendor_path @@ -134,6 +142,7 @@ class Chef def download_cookbook_to(download_path) downloader = Chef::Knife::CookbookSiteDownload.new downloader.config[:file] = download_path + downloader.config[:supermarket_site] = config[:supermarket_site] downloader.name_args = name_args downloader.run downloader @@ -141,17 +150,7 @@ class Chef def extract_cookbook(upstream_file, version) ui.info("Uncompressing #{@cookbook_name} version #{version}.") - extract_command = "tar zxvf \"#{convert_path upstream_file}\"" - if Chef::Platform.windows? - tar_version = shell_out("tar --version").stdout.tr("\n", " ") - if tar_version =~ /GNU tar/ - Chef::Log.debug("GNU tar detected, adding --force-local") - extract_command << " --force-local" - else - Chef::Log.debug("non-GNU tar detected, not adding --force-local") - end - end - shell_out!(extract_command, :cwd => @install_path) + Mixlib::Archive.new(convert_path(upstream_file)).extract(@install_path, perms: false) end def clear_existing_files(cookbook_path) diff --git a/lib/chef/knife/cookbook_site_list.rb b/lib/chef/knife/cookbook_site_list.rb index abe48bf340..3bdef8abe5 100644 --- a/lib/chef/knife/cookbook_site_list.rb +++ b/lib/chef/knife/cookbook_site_list.rb @@ -30,6 +30,13 @@ class Chef :long => "--with-uri", :description => "Show corresponding URIs" + option :supermarket_site, + :short => "-m SUPERMARKET_SITE", + :long => "--supermarket-site SUPERMARKET_SITE", + :description => "Supermarket Site", + :default => "https://supermarket.chef.io", + :proc => Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } + def run if config[:with_uri] cookbooks = Hash.new @@ -41,7 +48,7 @@ class Chef end def get_cookbook_list(items = 10, start = 0, cookbook_collection = {}) - cookbooks_url = "https://supermarket.chef.io/api/v1/cookbooks?items=#{items}&start=#{start}" + cookbooks_url = "#{config[:supermarket_site]}/api/v1/cookbooks?items=#{items}&start=#{start}" cr = noauth_rest.get(cookbooks_url) cr["items"].each do |cookbook| cookbook_collection[cookbook["cookbook_name"]] = cookbook diff --git a/lib/chef/knife/cookbook_site_search.rb b/lib/chef/knife/cookbook_site_search.rb index ba4b873efc..d401844217 100644 --- a/lib/chef/knife/cookbook_site_search.rb +++ b/lib/chef/knife/cookbook_site_search.rb @@ -24,12 +24,19 @@ class Chef banner "knife cookbook site search QUERY (options)" category "cookbook site" + option :supermarket_site, + :short => "-m SUPERMARKET_SITE", + :long => "--supermarket-site SUPERMARKET_SITE", + :description => "Supermarket Site", + :default => "https://supermarket.chef.io", + :proc => Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } + def run output(search_cookbook(name_args[0])) end def search_cookbook(query, items = 10, start = 0, cookbook_collection = {}) - cookbooks_url = "https://supermarket.chef.io/api/v1/search?q=#{query}&items=#{items}&start=#{start}" + cookbooks_url = "#{config[:supermarket_site]}/api/v1/search?q=#{query}&items=#{items}&start=#{start}" cr = noauth_rest.get(cookbooks_url) cr["items"].each do |cookbook| cookbook_collection[cookbook["cookbook_name"]] = cookbook diff --git a/lib/chef/knife/cookbook_site_share.rb b/lib/chef/knife/cookbook_site_share.rb index 6f37568f5f..d55d6c123a 100644 --- a/lib/chef/knife/cookbook_site_share.rb +++ b/lib/chef/knife/cookbook_site_share.rb @@ -50,6 +50,13 @@ class Chef :default => false, :description => "Don't take action, only print what files will be uploaded to Supermarket." + option :supermarket_site, + :short => "-m SUPERMARKET_SITE", + :long => "--supermarket-site SUPERMARKET_SITE", + :description => "Supermarket Site", + :default => "https://supermarket.chef.io", + :proc => Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } + def run config[:cookbook_path] ||= Chef::Config[:cookbook_path] @@ -106,23 +113,17 @@ class Chef end def get_category(cookbook_name) - begin - data = noauth_rest.get("https://supermarket.chef.io/api/v1/cookbooks/#{@name_args[0]}") - if !data["category"] && data["error_code"] - ui.fatal("Received an error from Supermarket: #{data["error_code"]}. On the first time you upload it, you are required to specify the category you want to share this cookbook to.") - exit(1) - else - data["category"] - end - rescue => e - ui.fatal("Unable to reach Supermarket: #{e.message}. Increase log verbosity (-VV) for more information.") - Chef::Log.debug("\n#{e.backtrace.join("\n")}") - exit(1) - end + data = noauth_rest.get("#{config[:supermarket_site]}/api/v1/cookbooks/#{@name_args[0]}") + data["category"] + rescue => e + return "Other" if e.kind_of?(Net::HTTPServerException) && e.response.code == "404" + ui.fatal("Unable to reach Supermarket: #{e.message}. Increase log verbosity (-VV) for more information.") + Chef::Log.debug("\n#{e.backtrace.join("\n")}") + exit(1) end def do_upload(cookbook_filename, cookbook_category, user_id, user_secret_filename) - uri = "https://supermarket.chef.io/api/v1/cookbooks" + uri = "#{config[:supermarket_site]}/api/v1/cookbooks" category_string = Chef::JSONCompat.to_json({ "category" => cookbook_category }) diff --git a/lib/chef/knife/cookbook_site_show.rb b/lib/chef/knife/cookbook_site_show.rb index c0280cb318..ce153ca5a1 100644 --- a/lib/chef/knife/cookbook_site_show.rb +++ b/lib/chef/knife/cookbook_site_show.rb @@ -24,21 +24,32 @@ class Chef banner "knife cookbook site show COOKBOOK [VERSION] (options)" category "cookbook site" + option :supermarket_site, + :short => "-m SUPERMARKET_SITE", + :long => "--supermarket-site SUPERMARKET_SITE", + :description => "Supermarket Site", + :default => "https://supermarket.chef.io", + :proc => Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } + def run output(format_for_display(get_cookbook_data)) end + def supermarket_uri + "#{config[:supermarket_site]}/api/v1" + end + def get_cookbook_data case @name_args.length when 1 - noauth_rest.get("https://supermarket.chef.io/api/v1/cookbooks/#{@name_args[0]}") + noauth_rest.get("#{supermarket_uri}/cookbooks/#{@name_args[0]}") when 2 - noauth_rest.get("https://supermarket.chef.io/api/v1/cookbooks/#{@name_args[0]}/versions/#{name_args[1].tr('.', '_')}") + noauth_rest.get("#{supermarket_uri}/cookbooks/#{@name_args[0]}/versions/#{name_args[1].tr('.', '_')}") end end def get_cookbook_list(items = 10, start = 0, cookbook_collection = {}) - cookbooks_url = "https://supermarket.chef.io/api/v1/cookbooks?items=#{items}&start=#{start}" + cookbooks_url = "#{supermarket_uri}/cookbooks?items=#{items}&start=#{start}" cr = noauth_rest.get(cookbooks_url) cr["items"].each do |cookbook| cookbook_collection[cookbook["cookbook_name"]] = cookbook diff --git a/lib/chef/knife/cookbook_site_unshare.rb b/lib/chef/knife/cookbook_site_unshare.rb index 310f6ac41d..bdabff0b94 100644 --- a/lib/chef/knife/cookbook_site_unshare.rb +++ b/lib/chef/knife/cookbook_site_unshare.rb @@ -30,6 +30,13 @@ class Chef banner "knife cookbook site unshare COOKBOOK" category "cookbook site" + option :supermarket_site, + :short => "-m SUPERMARKET_SITE", + :long => "--supermarket-site SUPERMARKET_SITE", + :description => "Supermarket Site", + :default => "https://supermarket.chef.io", + :proc => Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } + def run @cookbook_name = @name_args[0] if @cookbook_name.nil? @@ -41,7 +48,7 @@ class Chef confirm "Do you really want to unshare all versions of the cookbook #{@cookbook_name}" begin - rest.delete "https://supermarket.chef.io/api/v1/cookbooks/#{@name_args[0]}" + rest.delete "#{config[:supermarket_site]}/api/v1/cookbooks/#{@name_args[0]}" rescue Net::HTTPServerException => e raise e unless e.message =~ /Forbidden/ ui.error "Forbidden: You must be the maintainer of #{@cookbook_name} to unshare it." diff --git a/lib/chef/knife/core/bootstrap_context.rb b/lib/chef/knife/core/bootstrap_context.rb index 48d2cb9e77..b2670f196b 100644 --- a/lib/chef/knife/core/bootstrap_context.rb +++ b/lib/chef/knife/core/bootstrap_context.rb @@ -54,7 +54,7 @@ class Chef end def client_d - @cliend_d ||= client_d_content + @client_d ||= client_d_content end def encrypted_data_bag_secret @@ -114,6 +114,16 @@ validation_client_name "#{@chef_config[:validation_client_name]}" client_rb << %Q{https_proxy "#{knife_config[:bootstrap_proxy]}"\n} end + if knife_config[:bootstrap_proxy_user] + client_rb << %Q{http_proxy_user "#{knife_config[:bootstrap_proxy_user]}"\n} + client_rb << %Q{https_proxy_user "#{knife_config[:bootstrap_proxy_user]}"\n} + end + + if knife_config[:bootstrap_proxy_pass] + client_rb << %Q{http_proxy_pass "#{knife_config[:bootstrap_proxy_pass]}"\n} + client_rb << %Q{https_proxy_pass "#{knife_config[:bootstrap_proxy_pass]}"\n} + end + if knife_config[:bootstrap_no_proxy] client_rb << %Q{no_proxy "#{knife_config[:bootstrap_no_proxy]}"\n} end diff --git a/lib/chef/knife/supermarket_download.rb b/lib/chef/knife/supermarket_download.rb new file mode 100644 index 0000000000..5657558591 --- /dev/null +++ b/lib/chef/knife/supermarket_download.rb @@ -0,0 +1,33 @@ +# +# Author:: Christopher Webber (<cwebber@chef.io>) +# Copyright:: Copyright (c) 2014 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/knife" +require "chef/knife/cookbook_site_download" + +class Chef + class Knife + class SupermarketDownload < Knife::CookbookSiteDownload + # Handle the subclassing (knife doesn't do this :() + dependency_loaders.concat(superclass.dependency_loaders) + options.merge!(superclass.options) + + banner "knife supermarket download COOKBOOK [VERSION] (options)" + category "supermarket" + end + end +end diff --git a/lib/chef/knife/supermarket_install.rb b/lib/chef/knife/supermarket_install.rb new file mode 100644 index 0000000000..7642e68181 --- /dev/null +++ b/lib/chef/knife/supermarket_install.rb @@ -0,0 +1,33 @@ +# +# Author:: Christopher Webber (<cwebber@chef.io>) +# Copyright:: Copyright (c) 2014 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/knife" +require "chef/knife/cookbook_site_install" + +class Chef + class Knife + class SupermarketInstall < Knife::CookbookSiteInstall + # Handle the subclassing (knife doesn't do this :() + dependency_loaders.concat(superclass.dependency_loaders) + options.merge!(superclass.options) + + banner "knife supermarket install COOKBOOK [VERSION] (options)" + category "supermarket" + end + end +end diff --git a/lib/chef/knife/supermarket_list.rb b/lib/chef/knife/supermarket_list.rb new file mode 100644 index 0000000000..f2bc98bd0e --- /dev/null +++ b/lib/chef/knife/supermarket_list.rb @@ -0,0 +1,33 @@ +# +# Author:: Christopher Webber (<cwebber@chef.io>) +# Copyright:: Copyright (c) 2014 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/knife" +require "chef/knife/cookbook_site_list" + +class Chef + class Knife + class SupermarketList < Knife::CookbookSiteList + # Handle the subclassing (knife doesn't do this :() + dependency_loaders.concat(superclass.dependency_loaders) + options.merge!(superclass.options) + + banner "knife supermarket list (options)" + category "supermarket" + end + end +end diff --git a/lib/chef/knife/supermarket_search.rb b/lib/chef/knife/supermarket_search.rb new file mode 100644 index 0000000000..3206b0cb80 --- /dev/null +++ b/lib/chef/knife/supermarket_search.rb @@ -0,0 +1,33 @@ +# +# Author:: Christopher Webber (<cwebber@chef.io>) +# Copyright:: Copyright (c) 2014 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/knife" +require "chef/knife/cookbook_site_search" + +class Chef + class Knife + class SupermarketSearch < Knife::CookbookSiteSearch + # Handle the subclassing (knife doesn't do this :() + dependency_loaders.concat(superclass.dependency_loaders) + options.merge!(superclass.options) + + banner "knife supermarket search QUERY (options)" + category "supermarket" + end + end +end diff --git a/lib/chef/knife/supermarket_share.rb b/lib/chef/knife/supermarket_share.rb new file mode 100644 index 0000000000..3109b9e794 --- /dev/null +++ b/lib/chef/knife/supermarket_share.rb @@ -0,0 +1,33 @@ +# +# Author:: Christopher Webber (<cwebber@chef.io>) +# Copyright:: Copyright (c) 2014 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/knife" +require "chef/knife/cookbook_site_share" + +class Chef + class Knife + class SupermarketShare < Knife::CookbookSiteShare + # Handle the subclassing (knife doesn't do this :() + dependency_loaders.concat(superclass.dependency_loaders) + options.merge!(superclass.options) + + banner "knife supermarket share COOKBOOK [CATEGORY] (options)" + category "supermarket" + end + end +end diff --git a/lib/chef/knife/supermarket_show.rb b/lib/chef/knife/supermarket_show.rb new file mode 100644 index 0000000000..2ad122143f --- /dev/null +++ b/lib/chef/knife/supermarket_show.rb @@ -0,0 +1,33 @@ +# +# Author:: Christopher Webber (<cwebber@chef.io>) +# Copyright:: Copyright (c) 2014 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/knife" +require "chef/knife/cookbook_site_show" + +class Chef + class Knife + class SupermarketShow < Knife::CookbookSiteShow + # Handle the subclassing (knife doesn't do this :() + dependency_loaders.concat(superclass.dependency_loaders) + options.merge!(superclass.options) + + banner "knife supermarket show COOKBOOK [VERSION] (options)" + category "supermarket" + end + end +end diff --git a/lib/chef/knife/supermarket_unshare.rb b/lib/chef/knife/supermarket_unshare.rb new file mode 100644 index 0000000000..fd48e172ce --- /dev/null +++ b/lib/chef/knife/supermarket_unshare.rb @@ -0,0 +1,33 @@ +# +# Author:: Christopher Webber (<cwebber@chef.io>) +# Copyright:: Copyright (c) 2014 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/knife" +require "chef/knife/cookbook_site_unshare" + +class Chef + class Knife + class SupermarketUnshare < Knife::CookbookSiteUnshare + # Handle the subclassing (knife doesn't do this :() + dependency_loaders.concat(superclass.dependency_loaders) + options.merge!(superclass.options) + + banner "knife supermarket unshare COOKBOOK (options)" + category "supermarket" + end + end +end diff --git a/lib/chef/node.rb b/lib/chef/node.rb index 8d77becbf0..54faab6d3e 100644 --- a/lib/chef/node.rb +++ b/lib/chef/node.rb @@ -43,6 +43,8 @@ class Chef def_delegators :attributes, :keys, :each_key, :each_value, :key?, :has_key? def_delegators :attributes, :rm, :rm_default, :rm_normal, :rm_override def_delegators :attributes, :default!, :normal!, :override!, :force_default!, :force_override! + def_delegators :attributes, :default_unless, :normal_unless, :override_unless, :set_unless + def_delegators :attributes, :read, :read!, :write, :write!, :unlink, :unlink! attr_accessor :recipe_list, :run_state, :override_runlist @@ -196,35 +198,18 @@ class Chef # might be missing def normal attributes.top_level_breadcrumb = nil - attributes.set_unless_value_present = false attributes.normal end - alias_method :set, :normal - - # Set a normal attribute of this node, auto-vivifying any mashes that are - # missing, but if the final value already exists, don't set it - def normal_unless - attributes.top_level_breadcrumb = nil - attributes.set_unless_value_present = true - attributes.normal + def set + Chef.log_deprecation("node.set is deprecated and will be removed in Chef 14, please use node.default/node.override (or node.normal only if you really need persistence)") + normal end - alias_method :set_unless, :normal_unless - # Set a default of this node, but auto-vivify any Mashes that might # be missing def default attributes.top_level_breadcrumb = nil - attributes.set_unless_value_present = false - attributes.default - end - - # Set a default attribute of this node, auto-vivifying any mashes that are - # missing, but if the final value already exists, don't set it - def default_unless - attributes.top_level_breadcrumb = nil - attributes.set_unless_value_present = true attributes.default end @@ -232,15 +217,6 @@ class Chef # might be missing def override attributes.top_level_breadcrumb = nil - attributes.set_unless_value_present = false - attributes.override - end - - # Set an override attribute of this node, auto-vivifying any mashes that - # are missing, but if the final value already exists, don't set it - def override_unless - attributes.top_level_breadcrumb = nil - attributes.set_unless_value_present = true attributes.override end @@ -262,7 +238,6 @@ class Chef def automatic_attrs attributes.top_level_breadcrumb = nil - attributes.set_unless_value_present = false attributes.automatic end @@ -290,8 +265,14 @@ class Chef end # Only works for attribute fetches, setting is no longer supported - def method_missing(symbol, *args) - attributes.send(symbol, *args) + # XXX: this should be deprecated + def method_missing(method, *args, &block) + attributes.public_send(method, *args, &block) + end + + # Fix respond_to + method so that it works with method_missing delegation + def respond_to_missing?(method, include_private = false) + attributes.respond_to?(method, false) end # Returns true if this Node expects a given recipe, false if not. diff --git a/lib/chef/node/attribute.rb b/lib/chef/node/attribute.rb index ab97cf99bf..f5fe89251d 100644 --- a/lib/chef/node/attribute.rb +++ b/lib/chef/node/attribute.rb @@ -19,6 +19,7 @@ require "chef/node/immutable_collections" require "chef/node/attribute_collections" +require "chef/decorator/unchain" require "chef/mixin/deep_merge" require "chef/log" @@ -132,6 +133,7 @@ class Chef :take, :take_while, :to_a, + :to_h, :to_hash, :to_set, :value?, @@ -187,8 +189,6 @@ class Chef attr_accessor :deep_merge_cache def initialize(normal, default, override, automatic) - @set_unless_present = false - @default = VividMash.new(self, default) @env_default = VividMash.new(self, {}) @role_default = VividMash.new(self, {}) @@ -214,15 +214,13 @@ class Chef # attribute you're interested in. For example, to debug where the value # of `node[:network][:default_interface]` is coming from, use: # debug_value(:network, :default_interface). - # The return value is an Array of Arrays. The first element is - # `["set_unless_enabled?", Boolean]`, which describes whether the - # attribute collection is in "set_unless" mode. The rest of the Arrays + # The return value is an Array of Arrays. The Arrays # are pairs of `["precedence_level", value]`, where precedence level is # the component, such as role default, normal, etc. and value is the # attribute value set at that precedence level. If there is no value at # that precedence level, +value+ will be the symbol +:not_present+. def debug_value(*args) - components = COMPONENTS.map do |component| + COMPONENTS.map do |component| ivar = instance_variable_get(component) value = args.inject(ivar) do |so_far, key| if so_far == :not_present @@ -235,12 +233,6 @@ class Chef end [component.to_s.sub(/^@/, ""), value] end - [["set_unless_enabled?", @set_unless_present]] + components - end - - # Enables or disables `||=`-like attribute setting. See, e.g., Node#set_unless - def set_unless_value_present=(setting) - @set_unless_present = setting end # Invalidate a key in the deep_merge_cache. If called with nil, or no arg, this will invalidate @@ -321,94 +313,134 @@ class Chef # clears attributes from all precedence levels def rm(*args) - reset(args[0]) - # just easier to compute our retval, rather than collect+merge sub-retvals - ret = args.inject(merged_attributes) do |attr, arg| - if attr.nil? || !attr.respond_to?(:[]) - nil - else - begin - attr[arg] - rescue TypeError - raise TypeError, "Wrong type in index of attribute (did you use a Hash index on an Array?)" - end - end + with_deep_merged_return_value(self, *args) do + rm_default(*args) + rm_normal(*args) + rm_override(*args) end - rm_default(*args) - rm_normal(*args) - rm_override(*args) - ret end - # does <level>['foo']['bar'].delete('baz') - def remove_from_precedence_level(level, *args, key) - multimash = level.element(*args) - multimash.nil? ? nil : multimash.delete(key) - end - - private :remove_from_precedence_level - # clears attributes from all default precedence levels # - # equivalent to: force_default!['foo']['bar'].delete('baz') + # similar to: force_default!['foo']['bar'].delete('baz') + # - does not autovivify + # - does not trainwreck if interior keys do not exist def rm_default(*args) - reset(args[0]) - remove_from_precedence_level(force_default!(autovivify: false), *args) + with_deep_merged_return_value(combined_default, *args) do + default.unlink(*args) + role_default.unlink(*args) + env_default.unlink(*args) + force_default.unlink(*args) + end end # clears attributes from normal precedence # # equivalent to: normal!['foo']['bar'].delete('baz') + # - does not autovivify + # - does not trainwreck if interior keys do not exist def rm_normal(*args) - reset(args[0]) - remove_from_precedence_level(normal!(autovivify: false), *args) + normal.unlink(*args) end # clears attributes from all override precedence levels # # equivalent to: force_override!['foo']['bar'].delete('baz') + # - does not autovivify + # - does not trainwreck if interior keys do not exist def rm_override(*args) - reset(args[0]) - remove_from_precedence_level(force_override!(autovivify: false), *args) + with_deep_merged_return_value(combined_override, *args) do + override.unlink(*args) + role_override.unlink(*args) + env_override.unlink(*args) + force_override.unlink(*args) + end + end + + def with_deep_merged_return_value(obj, *path, last) + hash = obj.read(*path) + return nil unless hash.is_a?(Hash) + ret = hash[last] + yield + ret end + private :with_deep_merged_return_value + # # Replacing attributes without merging # # sets default attributes without merging - def default!(opts = {}) - # FIXME: do not flush whole cache - reset - MultiMash.new(self, @default, [], opts) + # + # - this API autovivifies (and cannot trainwreck) + def default!(*args) + return Decorator::Unchain.new(self, :default!) unless args.length > 0 + write(:default, *args) end # sets normal attributes without merging - def normal!(opts = {}) - # FIXME: do not flush whole cache - reset - MultiMash.new(self, @normal, [], opts) + # + # - this API autovivifies (and cannot trainwreck) + def normal!(*args) + return Decorator::Unchain.new(self, :normal!) unless args.length > 0 + write(:normal, *args) end # sets override attributes without merging - def override!(opts = {}) - # FIXME: do not flush whole cache - reset - MultiMash.new(self, @override, [], opts) + # + # - this API autovivifies (and cannot trainwreck) + def override!(*args) + return Decorator::Unchain.new(self, :override!) unless args.length > 0 + write(:override, *args) end # clears from all default precedence levels and then sets force_default - def force_default!(opts = {}) - # FIXME: do not flush whole cache - reset - MultiMash.new(self, @force_default, [@default, @env_default, @role_default], opts) + # + # - this API autovivifies (and cannot trainwreck) + def force_default!(*args) + return Decorator::Unchain.new(self, :force_default!) unless args.length > 0 + value = args.pop + rm_default(*args) + write(:force_default, *args, value) end # clears from all override precedence levels and then sets force_override - def force_override!(opts = {}) - # FIXME: do not flush whole cache - reset - MultiMash.new(self, @force_override, [@override, @env_override, @role_override], opts) + def force_override!(*args) + return Decorator::Unchain.new(self, :force_override!) unless args.length > 0 + value = args.pop + rm_override(*args) + write(:force_override, *args, value) + end + + # method-style access to attributes + + def read(*path) + merged_attributes.read(*path) + end + + def read!(*path) + merged_attributes.read!(*path) + end + + def exist?(*path) + merged_attributes.exist?(*path) + end + + def write(level, *args, &block) + self.send(level).write(*args, &block) + end + + def write!(level, *args, &block) + self.send(level).write!(*args, &block) + end + + def unlink(level, *path) + self.send(level).unlink(*path) + end + + def unlink!(level, *path) + self.send(level).unlink!(*path) end # @@ -420,9 +452,9 @@ class Chef # def merged_attributes(*path) - # immutablize( + # immutablize( merge_all(path) - # ) + # ) end def combined_override(*path) @@ -433,6 +465,27 @@ class Chef immutablize(merge_defaults(path)) end + def normal_unless(*args) + return Decorator::Unchain.new(self, :normal_unless) unless args.length > 0 + write(:normal, *args) if read(*args[0...-1]).nil? + end + + def default_unless(*args) + return Decorator::Unchain.new(self, :default_unless) unless args.length > 0 + write(:default, *args) if read(*args[0...-1]).nil? + end + + def override_unless(*args) + return Decorator::Unchain.new(self, :override_unless) unless args.length > 0 + write(:override, *args) if read(*args[0...-1]).nil? + end + + def set_unless(*args) + Chef.log_deprecation("node.set_unless is deprecated and will be removed in Chef 14, please use node.default_unless/node.override_unless (or node.normal_unless if you really need persistence)") + return Decorator::Unchain.new(self, :default_unless) unless args.length > 0 + write(:normal, *args) if read(*args[0...-1]).nil? + end + def [](key) if deep_merge_cache.has_key?(key.to_s) # return the cache of the deep merged values by top-level key @@ -461,13 +514,17 @@ class Chef alias :each_attribute :each def method_missing(symbol, *args) - if args.empty? + if symbol == :to_ary + merged_attributes.send(symbol, *args) + elsif args.empty? + Chef.log_deprecation %q{method access to node attributes (node.foo.bar) is deprecated and will be removed in Chef 13, please use bracket syntax (node["foo"]["bar"])} if key?(symbol) self[symbol] else raise NoMethodError, "Undefined method or attribute `#{symbol}' on `node'" end elsif symbol.to_s =~ /=$/ + Chef.log_deprecation %q{method setting of node attributes (node.foo="bar") is deprecated and will be removed in Chef 13, please use bracket syntax (node["foo"]="bar")} key_to_set = symbol.to_s[/^(.+)=$/, 1] self[key_to_set] = (args.length == 1 ? args[0] : args) else @@ -485,10 +542,6 @@ class Chef }.join(", ") << ">" end - def set_unless? - @set_unless_present - end - private # Helper method for merge_all/merge_defaults/merge_overrides. diff --git a/lib/chef/node/attribute_collections.rb b/lib/chef/node/attribute_collections.rb index 68f3a69756..b739ea8490 100644 --- a/lib/chef/node/attribute_collections.rb +++ b/lib/chef/node/attribute_collections.rb @@ -16,15 +16,15 @@ # limitations under the License. # +require "chef/node/common_api" + class Chef class Node - # == AttrArray # AttrArray is identical to Array, except that it keeps a reference to the # "root" (Chef::Node::Attribute) object, and will trigger a cache # invalidation on that object when mutated. class AttrArray < Array - MUTATOR_METHODS = [ :<<, :[]=, @@ -62,8 +62,9 @@ class Chef # Node::Attribute object. MUTATOR_METHODS.each do |mutator| define_method(mutator) do |*args, &block| + ret = super(*args, &block) root.reset_cache(root.top_level_breadcrumb) - super(*args, &block) + ret end end @@ -96,14 +97,12 @@ class Chef # in the creation of a new VividMash for that key. (This only works when # using the element reference method, `[]` -- other methods, such as # #fetch, work as normal). - # * It supports a set_unless flag (via the root Attribute object) which - # allows `||=` style behavior (`||=` does not work with - # auto-vivification). This is only implemented for #[]=; methods such as - # #store work as normal. # * attr_accessor style element set and get are supported via method_missing class VividMash < Mash attr_reader :root + include CommonAPI + # Methods that mutate a VividMash. Each of them is overridden so that it # also invalidates the cached merged_attributes on the root Attribute # object. @@ -148,12 +147,9 @@ class Chef def []=(key, value) root.top_level_breadcrumb ||= key - if set_unless? && key?(key) && !self[key].nil? - self[key] - else - root.reset_cache(root.top_level_breadcrumb) - super - end + ret = super + root.reset_cache(root.top_level_breadcrumb) + ret end alias :attribute? :has_key? @@ -176,10 +172,6 @@ class Chef end end - def set_unless? - @root.set_unless? - end - def convert_key(key) super end @@ -206,118 +198,5 @@ class Chef end end - - # == MultiMash - # This is a Hash-like object that contains multiple VividMashes in it. Its - # purpose is so that the user can descend into the mash and delete a subtree - # from all of the Mash objects (used to delete all values in a subtree from - # default, force_default, role_default and env_default at the same time). The - # assignment operator strictly does assignment (does no merging) and works - # by deleting the subtree and then assigning to the last mash which passed in - # the initializer. - # - # A lot of the complexity of this class comes from the fact that at any key - # value some or all of the mashes may walk off their ends and become nil or - # true or something. The schema may change so that one precidence leve may - # be 'true' object and another may be a VividMash. It is also possible that - # one or many of them may transition from VividMashes to Hashes or Arrays. - # - # It also supports the case where you may be deleting a key using node.rm - # in which case if intermediate keys all walk off into nil then you don't want - # to be autovivifying keys as you go. On the other hand you may be using - # node.force_default! in which case you'll wind up with a []= operator at the - # end and you want autovivification, so we conditionally have to support either - # operation. - # - # @todo: can we have an autovivify class that decorates a class that doesn't - # autovivify or something so that the code is less awful? - # - class MultiMash - attr_reader :root - attr_reader :mashes - attr_reader :opts - attr_reader :primary_mash - - # Initialize with an array of mashes. For the delete return value to work - # properly the mashes must come from the same attribute level (i.e. all - # override or all default, but not a mix of both). - def initialize(root, primary_mash, mashes, opts = {}) - @root = root - @primary_mash = primary_mash - @mashes = mashes - @opts = opts - @opts[:autovivify] = true if @opts[:autovivify].nil? - end - - def [](key) - # handle the secondary mashes - new_mashes = [] - mashes.each do |mash| - new_mash = safe_evalute_key(mash, key) - # secondary mashes never autovivify so once they fall into nil, we just stop tracking them - new_mashes.push(new_mash) unless new_mash.nil? - end - - new_primary_mash = safe_evalute_key(primary_mash, key) - - if new_primary_mash.nil? && @opts[:autovivify] - primary_mash[key] = VividMash.new(root) - new_primary_mash = primary_mash[key] - end - - MultiMash.new(root, new_primary_mash, new_mashes, opts) - end - - def []=(key, value) - if primary_mash.nil? - # This theoretically should never happen since node#force_default! setter methods will autovivify and - # node#rm methods do not end in #[]= operators. - raise TypeError, "No autovivification was specified initially on a method chain ending in assignment" - end - ret = delete(key) - primary_mash[key] = value - ret - end - - # mash.element('foo', 'bar') is the same as mash['foo']['bar'] - def element(key = nil, *subkeys) - return self if key.nil? - submash = self[key] - subkeys.empty? ? submash : submash.element(*subkeys) - end - - def delete(key) - # the return value is a deep merge which is correct semantics when - # merging between attributes on the same level (this would be incorrect - # if passed both override and default attributes which would need hash_only - # merging). - ret = mashes.inject(Mash.new) do |merged, mash| - Chef::Mixin::DeepMerge.merge(merged, mash) - end - ret = Chef::Mixin::DeepMerge.merge(ret, primary_mash) - mashes.each do |mash| - mash.delete(key) if mash.respond_to?(:delete) - end - primary_mash.delete(key) if primary_mash.respond_to?(:delete) - ret[key] - end - - private - - def safe_evalute_key(mash, key) - if mash.respond_to?(:[]) - if mash.respond_to?(:has_key?) - if mash.has_key?(key) - return mash[key] if mash[key].respond_to?(:[]) - end - elsif !mash[key].nil? - return mash[key] if mash[key].respond_to?(:[]) - end - end - return nil - end - - end - end end diff --git a/lib/chef/node/common_api.rb b/lib/chef/node/common_api.rb new file mode 100644 index 0000000000..ce2c6b6878 --- /dev/null +++ b/lib/chef/node/common_api.rb @@ -0,0 +1,129 @@ +#-- +# Copyright:: Copyright 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 Node + # shared API between VividMash and ImmutableMash, writer code can be + # 'shared' to keep it logically in this file by adding them to the + # block list in ImmutableMash. + module CommonAPI + # method-style access to attributes + + def valid_container?(obj, key) + return obj.is_a?(Hash) || (obj.is_a?(Array) && key.is_a?(Fixnum)) + end + + private :valid_container? + + # - autovivifying / autoreplacing writer + # - non-container-ey intermediate objects are replaced with hashes + def write(*args, &block) + root.top_level_breadcrumb = nil if respond_to?(:root) + value = block_given? ? yield : args.pop + last = args.pop + prev_memo = prev_key = nil + chain = args.inject(self) do |memo, key| + if !valid_container?(memo, key) + prev_memo[prev_key] = {} + memo = prev_memo[prev_key] + end + prev_memo = memo + prev_key = key + memo[key] + end + if !valid_container?(chain, last) + prev_memo[prev_key] = {} + chain = prev_memo[prev_key] + end + chain[last] = value + end + + # this autovivifies, but can throw NoSuchAttribute when trying to access #[] on + # something that is not a container ("schema violation" issues). + # + def write!(*args, &block) + root.top_level_breadcrumb = nil if respond_to?(:root) + value = block_given? ? yield : args.pop + last = args.pop + obj = args.inject(self) do |memo, key| + raise Chef::Exceptions::AttributeTypeMismatch unless valid_container?(memo, key) + memo[key] + end + raise Chef::Exceptions::AttributeTypeMismatch unless valid_container?(obj, last) + obj[last] = value + end + + # FIXME:(?) does anyone need a non-autovivifying writer for attributes that throws exceptions? + + # return true or false based on if the attribute exists + def exist?(*path) + root.top_level_breadcrumb = nil if respond_to?(:root) + path.inject(self) do |memo, key| + return false unless valid_container?(memo, key) + if memo.is_a?(Hash) + if memo.key?(key) + memo[key] + else + return false + end + elsif memo.is_a?(Array) + if memo.length > key + memo[key] + else + return false + end + end + end + return true + end + + # this is a safe non-autovivifying reader that returns nil if the attribute does not exist + def read(*path) + begin + read!(*path) + rescue Chef::Exceptions::NoSuchAttribute + nil + end + end + + # non-autovivifying reader that throws an exception if the attribute does not exist + def read!(*path) + raise Chef::Exceptions::NoSuchAttribute unless exist?(*path) + root.top_level_breadcrumb = nil if respond_to?(:root) + path.inject(self) do |memo, key| + memo[key] + end + end + + # FIXME:(?) does anyone really like the autovivifying reader that we have and wants the same behavior? readers that write? ugh... + + def unlink(*path, last) + root.top_level_breadcrumb = nil if respond_to?(:root) + hash = path.empty? ? self : read(*path) + return nil unless hash.is_a?(Hash) || hash.is_a?(Array) + root.top_level_breadcrumb ||= last + hash.delete(last) + end + + def unlink!(*path) + raise Chef::Exceptions::NoSuchAttribute unless exist?(*path) + unlink(*path) + end + + end + end +end diff --git a/lib/chef/node/immutable_collections.rb b/lib/chef/node/immutable_collections.rb index b5fd86fa72..d4623ace2a 100644 --- a/lib/chef/node/immutable_collections.rb +++ b/lib/chef/node/immutable_collections.rb @@ -1,3 +1,21 @@ +#-- +# 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 "chef/node/common_api" class Chef class Node @@ -124,6 +142,7 @@ class Chef class ImmutableMash < Mash include Immutablize + include CommonAPI alias :internal_set :[]= private :internal_set @@ -144,6 +163,10 @@ class Chef :replace, :select!, :shift, + :write, + :write!, + :unlink, + :unlink!, ] def initialize(mash_data) @@ -167,13 +190,15 @@ class Chef end def method_missing(symbol, *args) - if args.empty? + if symbol == :to_ary + super + elsif args.empty? if key?(symbol) self[symbol] else raise NoMethodError, "Undefined method or attribute `#{symbol}' on `node'" end - # This will raise a ImmutableAttributeModification error: + # This will raise a ImmutableAttributeModification error: elsif symbol.to_s =~ /=$/ key_to_set = symbol.to_s[/^(.+)=$/, 1] self[key_to_set] = (args.length == 1 ? args[0] : args) diff --git a/lib/chef/property.rb b/lib/chef/property.rb index 45ab4dd522..0589cb4c54 100644 --- a/lib/chef/property.rb +++ b/lib/chef/property.rb @@ -531,8 +531,6 @@ class Chef end end - protected - # # The options this Property will use for get/set behavior and validation. # @@ -583,6 +581,7 @@ class Chef (options.has_key?(:is) && resource.send(:_pv_is, { name => nil }, name, options[:is], raise_error: false)) end + # @api private def get_value(resource) if instance_variable_name resource.instance_variable_get(instance_variable_name) @@ -591,6 +590,7 @@ class Chef end end + # @api private def set_value(resource, value) if instance_variable_name resource.instance_variable_set(instance_variable_name, value) @@ -599,6 +599,7 @@ class Chef end end + # @api private def value_is_set?(resource) if instance_variable_name resource.instance_variable_defined?(instance_variable_name) @@ -607,6 +608,7 @@ class Chef end end + # @api private def reset_value(resource) if instance_variable_name if value_is_set?(resource) @@ -617,6 +619,8 @@ class Chef end end + private + def exec_in_resource(resource, proc, *args) if resource if proc.arity > args.size diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb index 03b546c09d..7cfddba0cb 100644 --- a/lib/chef/provider.rb +++ b/lib/chef/provider.rb @@ -423,9 +423,9 @@ class Chef module DeprecatedLWRPClass def const_missing(class_name) - if deprecated_constants[class_name.to_sym] + if Chef::Provider.deprecated_constants[class_name.to_sym] Chef.log_deprecation("Using an LWRP provider by its name (#{class_name}) directly is no longer supported in Chef 12 and will be removed. Use Chef::ProviderResolver.new(node, resource, action) instead.") - deprecated_constants[class_name.to_sym] + Chef::Provider.deprecated_constants[class_name.to_sym] else raise NameError, "uninitialized constant Chef::Provider::#{class_name}" end @@ -438,13 +438,12 @@ class Chef if Chef::Provider.const_defined?(class_name, false) Chef::Log.warn "Chef::Provider::#{class_name} already exists! Cannot create deprecation class for #{provider_class}" else - deprecated_constants[class_name.to_sym] = provider_class + Chef::Provider.deprecated_constants[class_name.to_sym] = provider_class end end - private - def deprecated_constants + raise "Deprecated constants should be called only on Chef::Provider" unless self == Chef::Provider @deprecated_constants ||= {} end end diff --git a/lib/chef/provider/batch.rb b/lib/chef/provider/batch.rb index bb294afd3f..0d857aaa79 100644 --- a/lib/chef/provider/batch.rb +++ b/lib/chef/provider/batch.rb @@ -29,7 +29,7 @@ class Chef end def command - basepath = is_forced_32bit ? wow64_directory : run_context.node.kernel.os_info.system_directory + basepath = is_forced_32bit ? wow64_directory : run_context.node["kernel"]["os_info"]["system_directory"] interpreter_path = Chef::Util::PathHelper.join(basepath, interpreter) diff --git a/lib/chef/provider/cron.rb b/lib/chef/provider/cron.rb index 36b67ab6a5..c7487cf42f 100644 --- a/lib/chef/provider/cron.rb +++ b/lib/chef/provider/cron.rb @@ -237,7 +237,7 @@ class Chef newcron = "" newcron << "# Chef Name: #{new_resource.name}\n" [ :mailto, :path, :shell, :home ].each do |v| - newcron << "#{v.to_s.upcase}=#{@new_resource.send(v)}\n" if @new_resource.send(v) + newcron << "#{v.to_s.upcase}=\"#{@new_resource.send(v)}\"\n" if @new_resource.send(v) end @new_resource.environment.each do |name, value| newcron << "#{name}=#{value}\n" diff --git a/lib/chef/provider/file.rb b/lib/chef/provider/file.rb index 7f85085eeb..bb0762ceb7 100644 --- a/lib/chef/provider/file.rb +++ b/lib/chef/provider/file.rb @@ -154,6 +154,7 @@ class Chef do_contents_changes do_acl_changes do_selinux + do_resolv_conf_fixup load_resource_attributes_from_file(@new_resource) end @@ -445,6 +446,13 @@ class Chef end end + def do_resolv_conf_fixup + # reload /etc/resolv.conf after we edit it -- only on linux -- and see lib/chef/application.rb + if new_resource.path == "/etc/resolv.conf" && RbConfig::CONFIG["host_os"] =~ /linux/ + Resolv::DefaultResolver.replace_resolvers [Resolv::DNS.new("/etc/resolv.conf")] + end + end + def do_acl_changes if access_controls.requires_changes? converge_by(access_controls.describe_changes) do diff --git a/lib/chef/provider/package/aix.rb b/lib/chef/provider/package/aix.rb index a1709c4af7..728f181055 100644 --- a/lib/chef/provider/package/aix.rb +++ b/lib/chef/provider/package/aix.rb @@ -55,7 +55,11 @@ class Chef ret = shell_out_with_timeout("installp -L -d #{@new_resource.source}") ret.stdout.each_line do |line| case line - when /#{@new_resource.package_name}:/ + when /:#{@new_resource.package_name}:/ + fields = line.split(":") + @new_resource.version(fields[2]) + when /^#{@new_resource.package_name}:/ + Chef::Log.warn("You are installing a bff package by product name. For idempotent installs, please install individual filesets") fields = line.split(":") @new_resource.version(fields[2]) end diff --git a/lib/chef/provider/package/openbsd.rb b/lib/chef/provider/package/openbsd.rb index 2120b9aa48..8043c01693 100644 --- a/lib/chef/provider/package/openbsd.rb +++ b/lib/chef/provider/package/openbsd.rb @@ -127,7 +127,7 @@ class Chef end def pkg_path - ENV["PKG_PATH"] || "http://ftp.OpenBSD.org/pub/#{node.kernel.name}/#{node.kernel.release}/packages/#{node.kernel.machine}/" + ENV["PKG_PATH"] || "http://ftp.OpenBSD.org/pub/#{node["kernel"]["name"]}/#{node["kernel"]["release"]}/packages/#{node["kernel"]["machine"]}/" end end diff --git a/lib/chef/provider/package/rubygems.rb b/lib/chef/provider/package/rubygems.rb index eb5a87099f..0aeec951b1 100644 --- a/lib/chef/provider/package/rubygems.rb +++ b/lib/chef/provider/package/rubygems.rb @@ -431,17 +431,23 @@ class Chef end def current_version - #raise 'todo' + # rubygems 2.6.3 ensures that gem lists are sorted newest first + pos = if Gem::Version.new(Gem::VERSION) >= Gem::Version.new("2.6.3") + :first + else + :last + end + # If one or more matching versions are installed, the newest of them # is the current version if !matching_installed_versions.empty? - gemspec = matching_installed_versions.last + gemspec = matching_installed_versions.send(pos) logger.debug { "#{@new_resource} found installed gem #{gemspec.name} version #{gemspec.version} matching #{gem_dependency}" } gemspec # If no version matching the requirements exists, the latest installed # version is the current version. elsif !all_installed_versions.empty? - gemspec = all_installed_versions.last + gemspec = all_installed_versions.send(pos) logger.debug { "#{@new_resource} newest installed version of gem #{gemspec.name} is #{gemspec.version}" } gemspec else diff --git a/lib/chef/provider/package/windows/exe.rb b/lib/chef/provider/package/windows/exe.rb index 70c9879845..211845c073 100644 --- a/lib/chef/provider/package/windows/exe.rb +++ b/lib/chef/provider/package/windows/exe.rb @@ -78,12 +78,9 @@ class Chef private def uninstall_command(uninstall_string) - uninstall_string.delete!('"') + uninstall_string = "\"#{uninstall_string}\"" if ::File.exist?(uninstall_string) uninstall_string = [ - %q{/d"}, - ::File.dirname(uninstall_string), - %q{" }, - ::File.basename(uninstall_string), + uninstall_string, expand_options(new_resource.options), " ", unattended_flags, diff --git a/lib/chef/provider/package/zypper.rb b/lib/chef/provider/package/zypper.rb index 5ee1dbea8e..e20a7332f7 100644 --- a/lib/chef/provider/package/zypper.rb +++ b/lib/chef/provider/package/zypper.rb @@ -38,15 +38,15 @@ class Chef status = shell_out_with_timeout!("zypper --non-interactive info #{package_name}") status.stdout.each_line do |line| case line - when /^Version: (.+)$/ - candidate_version = $1 - Chef::Log.debug("#{new_resource} version #{$1}") - when /^Installed: Yes$/ + when /^Version *: (.+) *$/ + candidate_version = $1.strip + Chef::Log.debug("#{new_resource} version #{candidate_version}") + when /^Installed *: Yes *$/ is_installed = true Chef::Log.debug("#{new_resource} is installed") - when /^Status: out-of-date \(version (.+) installed\)$/ - current_version = $1 - Chef::Log.debug("#{new_resource} out of date version #{$1}") + when /^Status *: out-of-date \(version (.+) installed\) *$/ + current_version = $1.strip + Chef::Log.debug("#{new_resource} out of date version #{current_version}") end end current_version = candidate_version if is_installed diff --git a/lib/chef/provider/powershell_script.rb b/lib/chef/provider/powershell_script.rb index 6365f6a171..ab85ec35ac 100644 --- a/lib/chef/provider/powershell_script.rb +++ b/lib/chef/provider/powershell_script.rb @@ -36,7 +36,7 @@ class Chef end def command - basepath = is_forced_32bit ? wow64_directory : run_context.node.kernel.os_info.system_directory + basepath = is_forced_32bit ? wow64_directory : run_context.node["kernel"]["os_info"]["system_directory"] # Powershell.exe is always in "v1.0" folder (for backwards compatibility) interpreter_path = Chef::Util::PathHelper.join(basepath, "WindowsPowerShell", "v1.0", interpreter) diff --git a/lib/chef/provider/remote_directory.rb b/lib/chef/provider/remote_directory.rb index e3bc579107..15b71c43bd 100644 --- a/lib/chef/provider/remote_directory.rb +++ b/lib/chef/provider/remote_directory.rb @@ -209,6 +209,8 @@ class Chef def cookbook_file_resource(target_path, relative_source_path) res = Chef::Resource::CookbookFile.new(target_path, run_context) res.cookbook_name = resource_cookbook + # Set the sensitivity level + res.sensitive(new_resource.sensitive) res.source(::File.join(source, relative_source_path)) if Chef::Platform.windows? && files_rights files_rights.each_pair do |permission, *args| diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index 2633187690..479eb0a7e2 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -260,6 +260,18 @@ class Chef end # + # Token class to hold an unresolved subscribes call with an associated + # run context. + # + # @api private + # @see Resource#subscribes + class UnresolvedSubscribes < self + # The full key ise given as the name in {Resource#subscribes} + alias_method :to_s, :name + alias_method :declared_key, :name + end + + # # Subscribes to updates from other resources, causing a particular action to # run on *this* resource when the other resource is updated. # @@ -326,7 +338,7 @@ class Chef resources = [resources].flatten resources.each do |resource| if resource.is_a?(String) - resource = Chef::Resource.new(resource, run_context) + resource = UnresolvedSubscribes.new(resource, run_context) end if resource.run_context.nil? resource.run_context = run_context @@ -1530,23 +1542,6 @@ class Chef end # @api private - def self.register_deprecated_lwrp_class(resource_class, class_name) - if Chef::Resource.const_defined?(class_name, false) - Chef::Log.warn "#{class_name} already exists! Deprecation class overwrites #{resource_class}" - Chef::Resource.send(:remove_const, class_name) - end - - if !Chef::Config[:treat_deprecation_warnings_as_errors] - Chef::Resource.const_set(class_name, resource_class) - deprecated_constants[class_name.to_sym] = resource_class - end - end - - def self.deprecated_constants - @deprecated_constants ||= {} - end - - # @api private def lookup_provider_constant(name, action = :nothing) begin self.class.provider_base.const_get(convert_to_class_name(name.to_s)) @@ -1559,6 +1554,27 @@ class Chef end end + module DeprecatedLWRPClass + + # @api private + def register_deprecated_lwrp_class(resource_class, class_name) + if Chef::Resource.const_defined?(class_name, false) + Chef::Log.warn "#{class_name} already exists! Deprecation class overwrites #{resource_class}" + Chef::Resource.send(:remove_const, class_name) + end + + if !Chef::Config[:treat_deprecation_warnings_as_errors] + Chef::Resource.const_set(class_name, resource_class) + Chef::Resource.deprecated_constants[class_name.to_sym] = resource_class + end + end + + def deprecated_constants + raise "Deprecated constants should be called only on Chef::Resource" unless self == Chef::Resource + @deprecated_constants ||= {} + end + end + private def self.remove_canonical_dsl @@ -1569,6 +1585,7 @@ class Chef end end end + extend DeprecatedLWRPClass end end diff --git a/lib/chef/resource_builder.rb b/lib/chef/resource_builder.rb index 138e401d5c..1641fe60f2 100644 --- a/lib/chef/resource_builder.rb +++ b/lib/chef/resource_builder.rb @@ -104,7 +104,11 @@ class Chef end def is_trivial_resource?(resource) - identicalish_resources?(resource_class.new(name, run_context), resource) + trivial_resource = resource_class.new(name, run_context) + # force un-lazy the name property on the created trivial resource + name_property = resource_class.properties.find { |sym, p| p.name_property? } + trivial_resource.send(name_property[0]) unless name_property.nil? + identicalish_resources?(trivial_resource, resource) end # this is an equality test specific to checking for 3694 cloning warnings @@ -124,9 +128,10 @@ class Chef end def emit_cloned_resource_warning - Chef::Log.warn("Cloning resource attributes for #{resource} from prior resource (CHEF-3694)") - Chef::Log.warn("Previous #{prior_resource}: #{prior_resource.source_line}") if prior_resource.source_line - Chef::Log.warn("Current #{resource}: #{resource.source_line}") if resource.source_line + message = "Cloning resource attributes for #{resource} from prior resource (CHEF-3694)" + message << "\nPrevious #{prior_resource}: #{prior_resource.source_line}" if prior_resource.source_line + message << "\nCurrent #{resource}: #{resource.source_line}" if resource.source_line + Chef.log_deprecation(message) end def emit_harmless_cloning_debug diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb index 29c936a932..7ef476c44b 100644 --- a/lib/chef/run_context.rb +++ b/lib/chef/run_context.rb @@ -194,12 +194,10 @@ class Chef # @param [Chef::Resource::Notification] The notification to add. # def notifies_before(notification) - nr = notification.notifying_resource - if nr.instance_of?(Chef::Resource) - before_notification_collection[nr.name] << notification - else - before_notification_collection[nr.declared_key] << notification - end + # Note for the future, notification.notifying_resource may be an instance + # of Chef::Resource::UnresolvedSubscribes when calling {Resource#subscribes} + # with a string value. + before_notification_collection[notification.notifying_resource.declared_key] << notification end # @@ -208,12 +206,10 @@ class Chef # @param [Chef::Resource::Notification] The notification to add. # def notifies_immediately(notification) - nr = notification.notifying_resource - if nr.instance_of?(Chef::Resource) - immediate_notification_collection[nr.name] << notification - else - immediate_notification_collection[nr.declared_key] << notification - end + # Note for the future, notification.notifying_resource may be an instance + # of Chef::Resource::UnresolvedSubscribes when calling {Resource#subscribes} + # with a string value. + immediate_notification_collection[notification.notifying_resource.declared_key] << notification end # @@ -222,12 +218,10 @@ class Chef # @param [Chef::Resource::Notification] The notification to add. # def notifies_delayed(notification) - nr = notification.notifying_resource - if nr.instance_of?(Chef::Resource) - delayed_notification_collection[nr.name] << notification - else - delayed_notification_collection[nr.declared_key] << notification - end + # Note for the future, notification.notifying_resource may be an instance + # of Chef::Resource::UnresolvedSubscribes when calling {Resource#subscribes} + # with a string value. + delayed_notification_collection[notification.notifying_resource.declared_key] << notification end # @@ -245,50 +239,29 @@ class Chef # # Get the list of before notifications sent by the given resource. # - # TODO seriously, this is actually wrong. resource.name is not unique, - # you need the type as well. - # # @return [Array[Notification]] # def before_notifications(resource) - if resource.instance_of?(Chef::Resource) - return before_notification_collection[resource.name] - else - return before_notification_collection[resource.declared_key] - end + return before_notification_collection[resource.declared_key] end # # Get the list of immediate notifications sent by the given resource. # - # TODO seriously, this is actually wrong. resource.name is not unique, - # you need the type as well. - # # @return [Array[Notification]] # def immediate_notifications(resource) - if resource.instance_of?(Chef::Resource) - return immediate_notification_collection[resource.name] - else - return immediate_notification_collection[resource.declared_key] - end + return immediate_notification_collection[resource.declared_key] end # # Get the list of delayed (end of run) notifications sent by the given # resource. # - # TODO seriously, this is actually wrong. resource.name is not unique, - # you need the type as well. - # # @return [Array[Notification]] # def delayed_notifications(resource) - if resource.instance_of?(Chef::Resource) - return delayed_notification_collection[resource.name] - else - return delayed_notification_collection[resource.declared_key] - end + return delayed_notification_collection[resource.declared_key] end # diff --git a/lib/chef/shell.rb b/lib/chef/shell.rb index aad5c49d00..26683cc25d 100644 --- a/lib/chef/shell.rb +++ b/lib/chef/shell.rb @@ -148,7 +148,7 @@ module Shell end def self.greeting - " #{Etc.getlogin}@#{Shell.session.node.fqdn}" + " #{Etc.getlogin}@#{Shell.session.node["fqdn"]}" rescue NameError, ArgumentError "" end diff --git a/lib/chef/version.rb b/lib/chef/version.rb index 7c38be31b3..cc29ef1740 100644 --- a/lib/chef/version.rb +++ b/lib/chef/version.rb @@ -21,7 +21,7 @@ class Chef CHEF_ROOT = File.expand_path("../..", __FILE__) - VERSION = "12.12.2" + VERSION = "12.13.21" end # |