lib/berkshelf/uploader.rb
require "chef/cookbook/chefignore"
require "chef/cookbook/cookbook_version_loader"
require "chef/cookbook_uploader"
require "chef/exceptions"
module Berkshelf
class Uploader
attr_reader :berksfile
attr_reader :lockfile
attr_reader :options
attr_reader :names
def initialize(berksfile, *args)
@berksfile = berksfile
@lockfile = berksfile.lockfile
opts = args.last.respond_to?(:to_hash) ? args.pop.to_hash.each_with_object({}) { |(k, v), m| m[k.to_sym] = v } : {}
@options = {
force: false,
freeze: true,
halt_on_frozen: false,
validate: true,
}.merge(opts)
@names = Array(args).flatten
end
def run
Berkshelf.log.info "Uploading cookbooks"
cookbooks = if names.empty?
Berkshelf.log.debug " No names given, using all cookbooks"
filtered_cookbooks
else
Berkshelf.log.debug " Names given (#{names.join(", ")})"
names.map { |name| lockfile.retrieve(name) }
end
# Perform all validations first to prevent partially uploaded cookbooks
Validator.validate_files(cookbooks)
upload(cookbooks)
cookbooks
end
private
# Upload the list of cookbooks to the Chef Server, with some exception
# wrapping.
#
# @param [Array<String>] cookbooks
def upload(cookbooks)
Berkshelf.log.info "Starting upload"
Berkshelf.ridley_connection(options) do |connection|
# this is a hack to work around a bug in chef 13.0-13.2 protocol negotiation on POST requests, its only
# use is to force protocol negotiation via a GET request -- it doesn't matter if it 404s. once we do not
# support those early 13.x versions this line can be safely deleted.
connection.get("users/#{Berkshelf.config.chef.node_name}") rescue nil
cookbooks.map do |cookbook|
begin
compiled_metadata = cookbook.compile_metadata
cookbook.reload if compiled_metadata
cookbook_version = cookbook.cookbook_version
Berkshelf.log.debug " Uploading #{cookbook.cookbook_name}"
cookbook_version.freeze_version if options[:freeze]
# another two lines that are necessary for chef < 13.2 support (affects 11.x/12.x as well)
cookbook_version.metadata.maintainer "" if cookbook_version.metadata.maintainer.nil?
cookbook_version.metadata.maintainer_email "" if cookbook_version.metadata.maintainer_email.nil?
begin
Chef::CookbookUploader.new(
[ cookbook_version ],
force: options[:force],
concurrency: 1, # sadly
rest: connection,
skip_syntax_check: options[:skip_syntax_check]
).upload_cookbooks
Berkshelf.formatter.uploaded(cookbook, connection)
rescue Chef::Exceptions::CookbookFrozen
if options[:halt_on_frozen]
raise FrozenCookbook.new(cookbook)
end
Berkshelf.formatter.skipping(cookbook, connection)
end
ensure
if compiled_metadata
# this is necessary on windows to clean up the ruby object that was pointing at the file
# so that we can reliably delete it. windows is terrible.
GC.start
File.unlink(compiled_metadata)
end
end
end
end
end
# Lookup dependencies in a cookbook and iterate to return dependencies of dependencies.
#
# This method is recursive. It iterates over a cookbook's dependencies
# and their dependencies in order to return an array of cookbooks, starting
# with the cookbook passed and followed by it's dependencies.
#
# @return [Array<CachedCookbook>]
#
def lookup_dependencies(cookbook, checked = {})
Berkshelf.log.debug " Looking up dependencies for #{cookbook}"
dependencies = []
lockfile.graph.find(cookbook).dependencies.each do |name, _|
next if checked[name]
# break cyclic graphs
checked[name] = true
# this is your standard depth-first tree traversal with the deps first...
dependencies += lookup_dependencies(name, checked)
# ..then the node itself
dependencies << name
end
dependencies
end
# Filter cookbooks based off the list of dependencies in the Berksfile.
#
# This method is secretly recursive. It iterates over each dependency in
# the Berksfile (using {Berksfile#dependencies} to account for filters)
# and retrieves that cookbook, it's dependencies, and the recusive
# dependencies, but iteratively.
#
# @return [Array<CachedCookbook>]
#
def filtered_cookbooks
# Create a copy of the dependencies. We need to make a copy, or else
# we would be adding dependencies directly to the Berksfile object, and
# that would be a bad idea...
dependencies = berksfile.dependencies.map(&:name)
checked = {}
cookbook_order = dependencies.map do |dependency|
# for each dep add all its deps first, then the dep itself
lookup_dependencies(dependency, checked) + [ dependency ]
end.flatten
cookbook_order.uniq.map { |dependency| lockfile.retrieve(dependency) }
end
end
end