# # Author:: Adam Jacob () # Author:: Christopher Walters () # Author:: Nuo Yan () # Copyright:: Copyright (c) 2009, 2010 Opscode, Inc. # 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/knife' class Chef class Knife class CookbookUpload < Knife CHECKSUM = "checksum" MATCH_CHECKSUM = /[0-9a-f]{32,}/ deps do require 'chef/exceptions' require 'chef/cookbook_loader' require 'chef/cookbook_uploader' end banner "knife cookbook upload [COOKBOOKS...] (options)" option :cookbook_path, :short => "-o PATH:PATH", :long => "--cookbook-path PATH:PATH", :description => "A colon-separated path to look for cookbooks in", :proc => lambda { |o| o.split(":") } option :freeze, :long => '--freeze', :description => 'Freeze this version of the cookbook so that it cannot be overwritten', :boolean => true option :all, :short => "-a", :long => "--all", :description => "Upload all cookbooks, rather than just a single cookbook" option :force, :long => '--force', :boolean => true, :description => "Update cookbook versions even if they have been frozen" option :environment, :short => '-E', :long => '--environment ENVIRONMENT', :description => "Set ENVIRONMENT's version dependency match the version you're uploading.", :default => nil option :depends, :short => "-d", :long => "--include-dependencies", :description => "Also upload cookbook dependencies" def run config[:cookbook_path] ||= Chef::Config[:cookbook_path] assert_environment_valid! warn_about_cookbook_shadowing version_constraints_to_update = {} # Get a list of cookbooks and their versions from the server # for checking existence of dependending cookbooks. @server_side_cookbooks = Chef::CookbookVersion.list if config[:all] justify_width = cookbook_repo.cookbook_names.map {|name| name.size}.max.to_i + 2 cookbook_repo.each do |cookbook_name, cookbook| cookbook.freeze_version if config[:freeze] upload(cookbook, justify_width) version_constraints_to_update[cookbook_name] = cookbook.version end else if @name_args.empty? show_usage ui.error("You must specify the --all flag or at least one cookbook name") exit 1 end justify_width = @name_args.map {|name| name.size }.max.to_i + 2 @name_args.each do |cookbook_name| begin cookbook = cookbook_repo[cookbook_name] if config[:depends] cookbook.metadata.dependencies.each do |dep, versions| @name_args.push dep end end cookbook.freeze_version if config[:freeze] upload(cookbook, justify_width) version_constraints_to_update[cookbook_name] = cookbook.version rescue Exceptions::CookbookNotFoundInRepo => e ui.error("Could not find cookbook #{cookbook_name} in your cookbook path, skipping it") Log.debug(e) end end end ui.info "upload complete" update_version_constraints(version_constraints_to_update) if config[:environment] end def cookbook_repo @cookbook_loader ||= begin Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::FileSystemFileVendor.new(manifest, config[:cookbook_path]) } Chef::CookbookLoader.new(config[:cookbook_path]) end end def update_version_constraints(new_version_constraints) new_version_constraints.each do |cookbook_name, version| environment.cookbook_versions[cookbook_name] = "= #{version}" end environment.save end def environment @environment ||= config[:environment] ? Environment.load(config[:environment]) : nil end def warn_about_cookbook_shadowing unless cookbook_repo.merged_cookbooks.empty? ui.warn "* " * 40 ui.warn(<<-WARNING) The cookbooks: #{cookbook_repo.merged_cookbooks.join(', ')} exist in multiple places in your cookbook_path. A composite version of these cookbooks has been compiled for uploading. #{ui.color('IMPORTANT:', :red, :bold)} In a future version of Chef, this behavior will be removed and you will no longer be able to have the same version of a cookbook in multiple places in your cookbook_path. WARNING ui.warn "The affected cookbooks are located:" ui.output ui.format_for_display(cookbook_repo.merged_cookbook_paths) ui.warn "* " * 40 end end private def assert_environment_valid! environment rescue Net::HTTPServerException => e if e.response.code.to_s == "404" ui.error "The environment #{config[:environment]} does not exist on the server, aborting." Log.debug(e) exit 1 else raise end end def upload(cookbook, justify_width) ui.info("Uploading #{cookbook.name.to_s.ljust(justify_width + 10)} [#{cookbook.version}]") check_for_broken_links(cookbook) check_dependencies(cookbook) Chef::CookbookUploader.new(cookbook, config[:cookbook_path], :force => config[:force]).upload_cookbook rescue Net::HTTPServerException => e case e.response.code when "409" ui.error "Version #{cookbook.version} of cookbook #{cookbook.name} is frozen. Use --force to override." Log.debug(e) else raise end end # if only you people wouldn't put broken symlinks in your cookbooks in # the first place. ;) def check_for_broken_links(cookbook) # MUST!! dup the cookbook version object--it memoizes its # manifest object, but the manifest becomes invalid when you # regenerate the metadata broken_files = cookbook.dup.manifest_records_by_path.select do |path, info| info[CHECKSUM].nil? || info[CHECKSUM] !~ MATCH_CHECKSUM end unless broken_files.empty? broken_filenames = Array(broken_files).map {|path, info| path} ui.error "The cookbook #{cookbook.name} has one or more broken files" ui.info "This is probably caused by broken symlinks in the cookbook directory" ui.info "The broken file(s) are: #{broken_filenames.join(' ')}" exit 1 end end def check_dependencies(cookbook) # for each dependency, check if the version is on the server, or # the version is in the cookbooks being uploaded. If not, exit and warn the user. cookbook.metadata.dependencies.each do |cookbook_name, version| unless check_server_side_cookbooks(cookbook_name, version) || check_uploading_cookbooks(cookbook_name, version) # warn the user and exit ui.error "Cookbook #{cookbook.name} depends on cookbook #{cookbook_name} version #{version}," ui.error "which is not currently being uploaded and cannot be found on the server." exit 1 end end end def check_server_side_cookbooks(cookbook_name, version) if @server_side_cookbooks[cookbook_name].nil? false else @server_side_cookbooks[cookbook_name]["versions"].each do |versions_hash| return true if Chef::VersionConstraint.new(version).include?(versions_hash["version"]) end false end end def check_uploading_cookbooks(cookbook_name, version) if config[:all] # check from all local cookbooks in the path unless cookbook_repo[cookbook_name].nil? return Chef::VersionConstraint.new(version).include?(cookbook_repo[cookbook_name].version) end else # check from only those in the command argument if @name_args.include?(cookbook_name) return Chef::VersionConstraint.new(version).include?(cookbook_repo[cookbook_name].version) end end false end end end end