module Ridley # @author Jamie Winsor class CookbookResource < Ridley::Resource class << self # List all of the cookbooks and their versions present on the remote # # @example return value # { # "ant" => [ # "0.10.1" # ], # "apache2" => [ # "1.4.0" # ] # } # # @param [Ridley::Client] client # # @return [Hash] # a hash containing keys which represent cookbook names and values which contain # an array of strings representing the available versions def all(client) response = client.connection.get(self.resource_path).body {}.tap do |cookbooks| response.each do |name, details| cookbooks[name] = details["versions"].collect { |version| version["version"] } end end end def create(*args) raise NotImplementedError end # Delete a cookbook of the given name and version on the remote Chef server # # @param [Ridley::Client] client # @param [String] name # @param [String] version # # @option options [Boolean] purge (false) # # @return [Boolean] def delete(client, name, version, options = {}) options = options.reverse_merge(purge: false) url = "#{self.resource_path}/#{name}/#{version}" url += "?purge=true" if options[:purge] client.connection.delete(url).body true rescue Errors::HTTPNotFound true end # Delete all of the versions of a given cookbook on the remote Chef server # # @param [Ridley::Client] client # @param [String] name # name of the cookbook to delete # # @option options [Boolean] purge (false) def delete_all(client, name, options = {}) versions(client, name).each do |version| delete(client, name, version, options) end end # Download the entire cookbook # # @param [Ridley::Client] client # @param [String] name # @param [String] version # @param [String] destination (Dir.mktmpdir) # the place to download the cookbook too. If no value is provided the cookbook # will be downloaded to a temporary location # # @return [String] # the path to the directory the cookbook was downloaded to def download(client, name, version, destination = Dir.mktmpdir) cookbook = find(client, name, version) unless cookbook.nil? cookbook.download(destination) end end # @param [Ridley::Client] client # @param [String, #chef_id] object # @param [String] version # # @return [nil, CookbookResource] def find(client, object, version) find!(client, object, version) rescue Errors::HTTPNotFound nil end # @param [Ridley::Client] client # @param [String, #chef_id] object # @param [String] version # # @raise [Errors::HTTPNotFound] # if a resource with the given chef_id is not found # # @return [CookbookResource] def find!(client, object, version) chef_id = object.respond_to?(:chef_id) ? object.chef_id : object new(client, client.connection.get("#{self.resource_path}/#{chef_id}/#{version}").body) end # Return the latest version of the given cookbook found on the remote Chef server # # @param [Ridley::Client] client # @param [String] name # # @return [String, nil] def latest_version(client, name) ver = versions(client, name).collect do |version| Solve::Version.new(version) end.sort.last ver.nil? ? nil : ver.to_s end # Return the version of the given cookbook which best stasifies the given constraint # # @param [Ridley::Client] client # @param [String] name # name of the cookbook # @param [String, Solve::Constraint] constraint # constraint to solve for # # @return [CookbookResource, nil] # returns the cookbook resource for the best solution or nil if no solution exists def satisfy(client, name, constraint) version = Solve::Solver.satisfy_best(constraint, versions(client, name)).to_s find(client, name, version) rescue Solve::Errors::NoSolutionError nil end # Save a new Cookbook Version of the given name, version with the # given manifest of files and checksums. # # @param [Ridley::Client] client # @param [String] name # @param [String] version # @param [String] manifest # a JSON blob containing file names, file paths, and checksums for each # that describe the cookbook version being uploaded. # # @option options [Boolean] :force # Upload the Cookbook even if the version already exists and is frozen on # the target Chef Server # @option options [Boolean] :freeze # Freeze the uploaded Cookbook on the Chef Server so that it cannot be # overwritten # # @return [Hash] def save(client, name, version, manifest, options = {}) options.reverse_merge(force: false, freeze: false) url = "cookbooks/#{name}/#{version}" url << "?force=true" if options[:force] client.connection.put(url, manifest) end def update(*args) raise NotImplementedError end # Uploads a cookbook to the remote Chef server from the contents of a filepath # # @param [Ridley::Client] client # @param [String] path # path to a cookbook on local disk # # @option options [String] :name # automatically populated by the metadata of the cookbook at the given path, but # in the event that the metadata does not contain a name it can be specified with # this option # @option options [Boolean] :force (false) # Upload the Cookbook even if the version already exists and is frozen on # the target Chef Server # @option options [Boolean] :freeze (false) # Freeze the uploaded Cookbook on the Chef Server so that it cannot be # overwritten # @option options [Boolean] :validate (true) # Validate the contents of the cookbook before uploading # # @return [Hash] def upload(client, path, options = {}) options = options.reverse_merge(validate: true, force: false, freeze: false) cookbook = Ridley::Chef::Cookbook.from_path(path, options.slice(:name)) if options[:validate] cookbook.validate end name = options[:name] || cookbook.name checksums = cookbook.checksums.dup sandbox = client.sandbox.create(checksums.keys) sandbox.upload(checksums) sandbox.commit save(client, name, cookbook.version, cookbook.to_json, options.slice(:force, :freeze)) end # Return a list of versions for the given cookbook present on the remote Chef server # # @param [Ridley::Client] client # @param [String] name # # @example # versions(client, "nginx") => [ "1.0.0", "1.2.0" ] # # @return [Array] def versions(client, name) response = client.connection.get("#{self.resource_path}/#{name}").body response[name]["versions"].collect do |cb_ver| cb_ver["version"] end end end include Ridley::Logging FILE_TYPES = [ :resources, :providers, :recipes, :definitions, :libraries, :attributes, :files, :templates, :root_files ].freeze set_chef_id "name" set_chef_type "cookbook" set_chef_json_class "Chef::Cookbook" set_resource_path "cookbooks" attribute :name, required: true attribute :attributes, type: Array, default: Array.new attribute :cookbook_name, type: String attribute :definitions, type: Array, default: Array.new attribute :files, type: Array, default: Array.new attribute :libraries, type: Array, default: Array.new attribute :metadata, type: Hashie::Mash attribute :providers, type: Array, default: Array.new attribute :recipes, type: Array, default: Array.new attribute :resources, type: Array, default: Array.new attribute :root_files, type: Array, default: Array.new attribute :templates, type: Array, default: Array.new attribute :version, type: String # Download the entire cookbook # # @param [String] destination (Dir.mktmpdir) # the place to download the cookbook too. If no value is provided the cookbook # will be downloaded to a temporary location # # @return [String] # the path to the directory the cookbook was downloaded to def download(destination = Dir.mktmpdir) destination = File.expand_path(destination) log.debug { "downloading cookbook: '#{name}'" } FILE_TYPES.each do |filetype| next unless manifest.has_key?(filetype) manifest[filetype].each do |file| file_destination = File.join(destination, file[:path].gsub('/', File::SEPARATOR)) FileUtils.mkdir_p(File.dirname(file_destination)) download_file(filetype, file[:path], file_destination) end end destination end # Download a single file from a cookbook # # @param [#to_sym] filetype # the type of file to download. These are broken up into the following types in Chef: # - attribute (unsupported until resolved https://github.com/reset/chozo/issues/17) # - definition # - file # - library # - provider # - recipe # - resource # - root_file # - template # these types are where the files are stored in your cookbook's structure. For example, a # recipe would be stored in the recipes directory while a root_file is stored at the root # of your cookbook # @param [String] path # path of the file to download # @param [String] destination # where to download the file to # # @return [nil] def download_file(filetype, path, destination) download_fun(filetype).call(path, destination) end # A hash containing keys for all of the different cookbook filetypes with values # representing each file of that type this cookbook contains # # @example # { # root_files: [ # { # :name => "afile.rb", # :path => "files/ubuntu-9.10/afile.rb", # :checksum => "2222", # :specificity => "ubuntu-9.10" # }, # ], # templates: [ manifest_record1, ... ], # ... # } # # @return [Hash] def manifest {}.tap do |manifest| FILE_TYPES.each do |filetype| manifest[filetype] = get_attribute(filetype) end end end def to_s "#{name}: #{manifest}" end private # Return a lambda for downloading a file from the cookbook of the given type # # @param [#to_sym] filetype # # @return [lambda] # a lambda which takes to parameters: target and path. Target is the URL to download from # and path is the location on disk to steam the contents of the remote URL to. def download_fun(filetype) collection = case filetype.to_sym when :attribute, :attributes; method(:attributes) when :definition, :definitions; method(:definitions) when :file, :files; method(:files) when :library, :libraries; method(:libraries) when :provider, :providers; method(:providers) when :recipe, :recipes; method(:recipes) when :resource, :resources; method(:resources) when :root_file, :root_files; method(:root_files) when :template, :templates; method(:templates) else raise Errors::UnknownCookbookFileType.new(filetype) end ->(target, destination) { files = collection.call # JW: always chaining .call.find results in a nil value. WHY? file = files.find { |f| f[:path] == target } return nil if file.nil? destination = File.expand_path(destination) log.debug { "downloading '#{filetype}' file: #{file} to: '#{destination}'" } client.connection.stream(file[:url], destination) } end end end