# License:: Apache License, Version 2.0## Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at## http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.#require_relative"../cookbook_version"require_relative"chefignore"require_relative"metadata"require_relative"../util/path_helper"require"find"unlessdefined?(Find.find)classChefclassCookbook# This class is only used directly from the Chef::CookbookLoader and from chef-fs,# so it only affects legacy-mode chef-client runs and knife. It is not used by# server or zolo/zero modes.## This seems to be mostly a glorified factory method for creating CookbookVersion# objects now, with creating Metadata objects bolted onto the side? It used# to be also responsible for the merging of multiple objects when creating# shadowed/merged cookbook versions from multiple sources. It also handles# Chefignore files.#classCookbookVersionLoaderUPLOADED_COOKBOOK_VERSION_FILE=".uploaded-cookbook-version.json".freezeattr_reader:cookbook_settingsattr_reader:frozenattr_reader:uploaded_cookbook_version_fileattr_reader:cookbook_path# The cookbook's name as inferred from its directory.attr_reader:inferred_cookbook_nameattr_reader:metadata_errordefinitialize(path,chefignore=nil)@cookbook_path=File.expand_path(path)# cookbook_path from which this was loaded@inferred_cookbook_name=File.basename(path)@chefignore=chefignore@metadata=nil@relative_path=%r{#{Regexp.escape(cookbook_path)}/(.+)$}@metadata_loaded=false@cookbook_settings={all_files: {},}@metadata_filenames=[]@metadata_error=nilend# Load the cookbook. Raises an error if the cookbook_path given to the# constructor doesn't point to a valid cookbook.defload!metadata# force lazy evaluation to occur# re-raise any exception that occurred when reading the metadataraise_metadata_error!load_all_filesremove_ignored_filesifempty?raiseExceptions::CookbookNotFoundInRepo,"The directory #{cookbook_path} does not contain a cookbook"endcookbook_settingsenddefloadChef.deprecated(:internal_api,"Chef::Cookbook::CookbookVersionLoader's load method is deprecated. Please use load! instead.")metadata# force lazy evaluation to occur# re-raise any exception that occurred when reading the metadataraise_metadata_error!load_all_filesremove_ignored_filesifempty?Chef::Log.warn"Found a directory #{cookbook_name} in the cookbook path, but it contains no cookbook files. skipping."endcookbook_settingsendalias:load_cookbooks:loaddefcookbook_versionreturnnilifempty?Chef::CookbookVersion.new(cookbook_name,cookbook_path).tapdo|c|c.all_files=cookbook_settings[:all_files].valuesc.metadata=metadatac.freeze_versioniffrozenendend# Generates the Cookbook::Metadata objectdefmetadatareturn@metadataunless@metadata.nil?@metadata=Chef::Cookbook::Metadata.newmetadata_filenames.eachdo|metadata_file|casemetadata_filewhen/\.rb$/apply_ruby_metadata(metadata_file)whenuploaded_cookbook_version_fileapply_json_cookbook_version_metadata(metadata_file)when/\.json$/apply_json_metadata(metadata_file)elseraise"Invalid metadata file: #{metadata_file} for cookbook: #{cookbook_version}"endend@metadata# Rescue errors so that users can upload cookbooks via `knife cookbook# upload` even if some cookbooks in their chef-repo have errors in# their metadata. We only rescue StandardError because you have to be# doing something *really* terrible to raise an exception that inherits# directly from Exception in your metadata.rb file.rescueStandardError=>e@metadata_error=e@metadataenddefcookbook_name# The `name` attribute is now required in metadata, so# inferred_cookbook_name generally should not be used. Per CHEF-2923,# we have to not raise errors in cookbook metadata immediately, so that# users can still `knife cookbook upload some-cookbook` when an# unrelated cookbook has an error in its metadata. This situation# could prevent us from reading the `name` attribute from the metadata# entirely, but the name is used as a hash key in CookbookLoader, so we# fall back to the inferred name here.(metadata.name||inferred_cookbook_name).to_symendprivatedefmetadata_filenamesreturn@metadata_filenamesunless@metadata_filenames.empty?ifFile.exist?(File.join(cookbook_path,UPLOADED_COOKBOOK_VERSION_FILE))@uploaded_cookbook_version_file=File.join(cookbook_path,UPLOADED_COOKBOOK_VERSION_FILE)endifFile.exist?(File.join(cookbook_path,"metadata.json"))@metadata_filenames<<File.join(cookbook_path,"metadata.json")elsifFile.exist?(File.join(cookbook_path,"metadata.rb"))@metadata_filenames<<File.join(cookbook_path,"metadata.rb")elsifuploaded_cookbook_version_file@metadata_filenames<<uploaded_cookbook_version_fileend# Set frozen based on .uploaded-cookbook-version.jsonset_frozen@metadata_filenamesenddefraise_metadata_error!raisemetadata_errorunlessmetadata_error.nil?# Metadata won't be valid if the cookbook is empty. If the cookbook is# actually empty, a metadata error here would be misleading, so don't# raise it (if called by #load!, a different error is raised).if!empty?&&!metadata.valid?message="Cookbook loaded at path [#{cookbook_path}] has invalid metadata: #{metadata.errors.join("; ")}"raiseExceptions::MetadataNotValid,messageendfalseenddefempty?cookbook_settings.values.all?(&:empty?)&&metadata_filenames.size==0enddefchefignore@chefignore||=Chefignore.new(cookbook_path)end# Enumerate all the files in a cookbook and assign the resulting list to# `cookbook_settings[:all_files]`. In order to behave in a compatible way# with previous implementations, directories at the cookbook's root that# begin with a dot are ignored. dotfiles are generally not ignored,# however if the file is named ".uploaded-cookbook-version.json" it is# assumed to be managed by chef-zero and not part of the cookbook.defload_all_filesreturnunlessFile.exist?(cookbook_path)# If cookbook_path is a symlink, Find on Windows Ruby 2.3 will not traverse it.# Dir.entries will do so on all platforms, so we iterate the top level using# Dir.entries. Since we have different behavior at the top anyway (hidden# directories at the top level are not included for backcompat), this# actually keeps things a bit cleaner.Dir.entries(cookbook_path).eachdo|top_filename|# Skip top-level directories starting with "."top_path=File.join(cookbook_path,top_filename)nextiftop_filename.start_with?(".")&&File.directory?(top_path)# Use Find.find because it:# (a) returns any children, recursively# (b) includes top_path as well# (c) skips symlinks, which is backcompat (no judgement on whether it was *right*)Find.find(top_path)do|path|# Only add files, not directoriesnextunlessFile.file?(path)# Don't add .uploaded-cookbook-version.jsonnextifFile.basename(path)==UPLOADED_COOKBOOK_VERSION_FILErelative_path=Chef::Util::PathHelper.relative_path_from(cookbook_path,path)path=Pathname.new(path).cleanpath.to_scookbook_settings[:all_files][relative_path]=pathendendenddefremove_ignored_filescookbook_settings[:all_files].reject!do|relative_path,full_path|chefignore.ignored?(relative_path)endenddefapply_ruby_metadata(file)@metadata.from_file(file)rescueChef::Exceptions::JSON::ParseErrorChef::Log.error("Error evaluating metadata.rb for #{inferred_cookbook_name} in "+file)raiseenddefapply_json_metadata(file)@metadata.from_json(IO.read(file))rescueChef::Exceptions::JSON::ParseErrorChef::Log.error("Couldn't parse cookbook metadata JSON for #{inferred_cookbook_name} in "+file)raiseenddefapply_json_cookbook_version_metadata(file)data=Chef::JSONCompat.parse(IO.read(file))@metadata.from_hash(data["metadata"])# the JSON cookbook metadata file is only used by chef-zero.# The Chef Server API currently does not enforce that the metadata# have a `name` field, but that will cause an error when attempting# to load the cookbook. To keep compatibility, we fake it by setting# the metadata name from the cookbook version object's name.## This behavior can be removed if/when Chef Server enforces that the# metadata contains a name key.@metadata.name(data["cookbook_name"])unlessdata["metadata"].key?("name")rescueChef::Exceptions::JSON::ParseErrorChef::Log.error("Couldn't parse cookbook metadata JSON for #{inferred_cookbook_name} in "+file)raiseenddefset_frozenifuploaded_cookbook_version_filebegindata=Chef::JSONCompat.parse(IO.read(uploaded_cookbook_version_file))@frozen=data["frozen?"]rescueChef::Exceptions::JSON::ParseErrorChef::Log.error("Couldn't parse cookbook metadata JSON for #{inferred_cookbook_name} in #{uploaded_cookbook_version_file}")raiseendendendendendend