lib/ridley/resources/cookbook_resource.rb
require 'ridley/helpers'
module Ridley
class CookbookResource < Ridley::Resource
task_class TaskThread
set_resource_path "cookbooks"
represented_by Ridley::CookbookObject
def initialize(connection_registry, client_name, client_key, options = {})
super(connection_registry)
@sandbox_resource = SandboxResource.new_link(connection_registry, client_name, client_key, options)
end
# List all of the cookbooks and their versions present on the remote
#
# @example return value
# {
# "ant" => [
# "0.10.1"
# ],
# "apache2" => [
# "1.4.0"
# ]
# }
#
# @return [Hash]
# a hash containing keys which represent cookbook names and values which contain
# an array of strings representing the available versions
def all
response = request(:get, self.class.resource_path, num_versions: "all")
{}.tap do |cookbooks|
response.each do |name, details|
cookbooks[name] = details["versions"].collect { |version| version["version"] }
end
end
end
# Delete a cookbook of the given name and version on the remote Chef server
#
# @param [String] name
# @param [String] version
#
# @option options [Boolean] purge (false)
#
# @return [Boolean]
def delete(name, version, options = {})
options = options.reverse_merge(purge: false)
url = "#{self.class.resource_path}/#{name}/#{version}"
url += "?purge=true" if options[:purge]
request(:delete, url)
true
rescue AbortError => ex
return nil if ex.cause.is_a?(Errors::HTTPNotFound)
abort(ex.cause)
end
# Delete all of the versions of a given cookbook on the remote Chef server
#
# @param [String] name
# name of the cookbook to delete
#
# @option options [Boolean] purge (false)
def delete_all(name, options = {})
versions(name).collect { |version| future(:delete, name, version, options) }.map(&:value)
end
# Download the entire cookbook
#
# @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
#
# @raise [Errors::ResourceNotFound] if the target cookbook is not found
#
# @return [String]
# the path to the directory the cookbook was downloaded to
def download(name, version, destination = Dir.mktmpdir)
if cookbook = find(name, version)
cookbook.download(destination)
else
abort Errors::ResourceNotFound.new("cookbook #{name} (#{version}) was not found")
end
end
# @param [String, #chef_id] object
# @param [String] version
#
# @return [nil, CookbookResource]
def find(object, version)
chef_id = object.respond_to?(:chef_id) ? object.chef_id : object
new(request(:get, "#{self.class.resource_path}/#{chef_id}/#{version}"))
rescue AbortError => ex
return nil if ex.cause.is_a?(Errors::HTTPNotFound)
abort(ex.cause)
end
# Return the latest version of the given cookbook found on the remote Chef server
#
# @param [String] name
#
# @raise [Errors::ResourceNotFound] if the target cookbook has no versions
#
# @return [String, nil]
def latest_version(name)
ver = versions(name).collect do |version|
Semverse::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 [String] name
# name of the cookbook
# @param [String, Semverse::Constraint] constraint
# constraint to solve for
#
# @raise [Errors::ResourceNotFound] if the target cookbook has no versions
#
# @return [CookbookResource, nil]
# returns the cookbook resource for the best solution or nil if no solution exists
def satisfy(name, constraint)
version = Semverse::Constraint.satisfy_best(constraint, versions(name)).to_s
find(name, version)
rescue Semverse::NoSolutionError
nil
end
# Update or create a new Cookbook Version of the given name, version with the
# given manifest of files and checksums.
#
# @param [Ridley::Chef::Cookbook] cookbook
# the cookbook to save
#
# @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
#
# @raise [Ridley::Errors::FrozenCookbook]
# if a cookbook of the same name and version already exists on the remote Chef server
# and is frozen. If the :force option is provided the given cookbook will be saved
# regardless.
#
# @return [Hash]
def update(cookbook, options = {})
options = options.reverse_merge(force: false, freeze: false)
cookbook.frozen = options[:freeze]
url = "cookbooks/#{cookbook.cookbook_name}/#{cookbook.version}"
url << "?force=true" if options[:force]
request(:put, url, cookbook.to_json)
rescue AbortError => ex
if ex.cause.is_a?(Errors::HTTPConflict)
abort Ridley::Errors::FrozenCookbook.new(ex)
end
abort(ex.cause)
end
alias_method :create, :update
# Uploads a cookbook to the remote Chef server from the contents of a filepath
#
# @param [String] path
# path to a cookbook on local disk
#
# @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(path, options = {})
options = options.reverse_merge(validate: true, force: false, freeze: false)
cookbook = Ridley::Chef::Cookbook.from_path(path)
unless (existing = find(cookbook.cookbook_name, cookbook.version)).nil?
if existing.frozen? && options[:force] == false
msg = "The cookbook #{cookbook.cookbook_name} (#{cookbook.version}) already exists and is"
msg << " frozen on the Chef server. Use the 'force' option to override."
abort Ridley::Errors::FrozenCookbook.new(msg)
end
end
if options[:validate]
cookbook.validate
end
# Compile metadata on upload if it hasn't been compiled already
unless cookbook.compiled_metadata?
compiled_metadata = cookbook.compile_metadata
cookbook.reload
end
# Skip uploading the raw metadata (metadata.rb). The raw metadata is unecessary for the
# client, and this is required until compiled metadata (metadata.json) takes precedence over
# raw metadata in the Chef-Client.
#
# We can change back to including the raw metadata in the future after this has been fixed or
# just remove these comments. There is no circumstance that I can currently think of where
# raw metadata should ever be read by the client.
#
# - Jamie
#
# See the following tickets for more information:
# * https://tickets.opscode.com/browse/CHEF-4811
# * https://tickets.opscode.com/browse/CHEF-4810
cookbook.manifest[:root_files].reject! do |file|
File.basename(file[:name]).downcase == Ridley::Chef::Cookbook::Metadata::RAW_FILE_NAME
end
checksums = cookbook.checksums.dup
sandbox = sandbox_resource.create(checksums.keys.sort)
sandbox.upload(checksums)
sandbox.commit
update(cookbook, Ridley::Helpers.options_slice(options, :force, :freeze))
ensure
# Destroy the compiled metadata only if it was created
if compiled_metadata
# The garbage collector still has a reference to the file we'd
# like to delete. On *nix this isn't a big deal, but on Windows
# [ ruby 2.0.0p451 (2014-02-24) [i386-mingw32] ] open files
# cannot be deleted, so we're forced to garbage collect to
# ensure we can delete the file. This is CRITICAL to ensure that
# a stale metadata file isn't left on disk because next time we
# would use that file instead of recompiling.
GC.start
File.delete(compiled_metadata)
end
end
# Return a list of versions for the given cookbook present on the remote Chef server
#
# @param [String] name
#
# @example
# versions("nginx") => [ "1.0.0", "1.2.0" ]
#
# @raise [Errors::ResourceNotFound] if the target cookbook has no versions
#
# @return [Array<String>]
def versions(name)
response = request(:get, "#{self.class.resource_path}/#{name}")
response[name]["versions"].collect do |cb_ver|
cb_ver["version"]
end
rescue AbortError => ex
if ex.cause.is_a?(Errors::HTTPNotFound)
abort Errors::ResourceNotFound.new(ex)
end
abort(ex.cause)
end
private
attr_reader :sandbox_resource
end
end