# # Author:: Adam Jacob () # Author:: Christopher Walters () # Author:: Daniel DeLeo () # Copyright:: Copyright 2008-2018, Chef Software Inc. # Copyright:: Copyright 2009-2016, Daniel DeLeo # 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/config" require "chef/exceptions" require "chef/cookbook/cookbook_version_loader" require "chef/cookbook_version" require "chef/cookbook/chefignore" require "chef/cookbook/metadata" class Chef # This class is used by knife, cheffs and legacy chef-solo modes. It is not used by the server mode # of chef-client or zolo/zero modes. # # This class implements orchestration around producing a single cookbook_version for a cookbook or # loading a Mash of all cookbook_versions, using the cookbook_version_loader class, and doing # lazy-access and memoization to only load each cookbook once on demand. # # This implements a key-value style each which makes it appear to be a Hash of String => CookbookVersion # pairs where the String is the cookbook name. The use of Enumerable combined with the Hash-style # each is likely not entirely sane. # # This object is also passed and injected into the CookbookCollection object where it is converted # to a Mash that looks almost exactly like the cookbook_by_name Mash in this object. # class CookbookLoader # @return [Array] the array of repo paths containing cookbook dirs attr_reader :repo_paths # XXX: this is highly questionable combined with the Hash-style each method include Enumerable # @param repo_paths [Array] the array of repo paths containing cookbook dirs def initialize(*repo_paths) @repo_paths = repo_paths.flatten.map { |p| File.expand_path(p) } raise ArgumentError, "You must specify at least one cookbook repo path" if repo_paths.empty? end # The primary function of this class is to build this Mash mapping cookbook names as a string to # the CookbookVersion objects for them. Callers must call "load_cookbooks" first. # # @return [Mash] def cookbooks_by_name @cookbooks_by_name ||= Mash.new end # This class also builds a mapping of cookbook names to their Metadata objects. Callers must call # "load_cookbooks" first. # # @return [Mash] def metadata @metadata ||= Mash.new end # Loads all cookbooks across all repo_paths # # @return [Mash] the cookbooks_by_name Mash def load_cookbooks cookbook_version_loaders.each_key do |cookbook_name| load_cookbook(cookbook_name) end cookbooks_by_name end # Loads a single cookbook by its name. # # @param [String] # @return [Chef::CookbookVersion] def load_cookbook(cookbook_name) unless cookbook_version_loaders.key?(cookbook_name) raise Exceptions::CookbookNotFoundInRepo, "Cannot find a cookbook named #{cookbook_name}; did you forget to add metadata to a cookbook? (https://docs.chef.io/config_rb_metadata.html)" end return cookbooks_by_name[cookbook_name] if cookbooks_by_name.key?(cookbook_name) loader = cookbook_version_loaders[cookbook_name] loader.load cookbook_version = loader.cookbook_version cookbooks_by_name[cookbook_name] = cookbook_version metadata[cookbook_name] = cookbook_version.metadata cookbook_version end def [](cookbook) load_cookbook(cookbook) end alias :fetch :[] def has_key?(cookbook_name) not self[cookbook_name.to_sym].nil? end alias :cookbook_exists? :has_key? alias :key? :has_key? def each cookbooks_by_name.keys.sort_by(&:to_s).each do |cname| yield(cname, cookbooks_by_name[cname]) end end def each_key(&block) cookbook_names.each(&block) end def each_value(&block) values.each(&block) end def cookbook_names cookbooks_by_name.keys.sort end def values cookbooks_by_name.values end alias :cookbooks :values private # Helper method to lazily create and remember the chefignore object # for a given repo_path. # # @param [String] repo_path the full path to the cookbook directory of the repo # @return [Chef::Cookbook::Chefignore] the chefignore object for the repo_path def chefignore(repo_path) @chefignores ||= {} @chefignores[repo_path] ||= Cookbook::Chefignore.new(repo_path) end def all_directories_in_repo_paths @all_directories_in_repo_paths ||= all_files_in_repo_paths.select { |path| File.directory?(path) } end def all_files_in_repo_paths @all_files_in_repo_paths ||= begin repo_paths.inject([]) do |all_children, repo_path| all_children + Dir[File.join(Chef::Util::PathHelper.escape_glob_dir(repo_path), "*")] end end end # This method creates a Mash of the CookbookVersionLoaders for each cookbook. # # @return [Mash] def cookbook_version_loaders @cookbook_version_loaders ||= begin mash = Mash.new all_directories_in_repo_paths.each do |cookbook_path| loader = Cookbook::CookbookVersionLoader.new(cookbook_path, chefignore(File.dirname(cookbook_path))) cookbook_name = loader.cookbook_name if mash.key?(cookbook_name) raise Chef::Exceptions::CookbookMergingError, "Cookbook merging is no longer supported, the cookbook named #{cookbook_name} can only appear once in the cookbook_path" end mash[cookbook_name] = loader end mash end end end end