diff options
-rw-r--r-- | chef-server-api/app/controllers/cookbooks.rb | 6 | ||||
-rw-r--r-- | chef/lib/chef/cookbook_loader.rb | 3 | ||||
-rw-r--r-- | chef/lib/chef/cookbook_uploader.rb | 21 | ||||
-rw-r--r-- | chef/lib/chef/cookbook_version.rb | 25 | ||||
-rw-r--r-- | chef/lib/chef/exceptions.rb | 3 | ||||
-rw-r--r-- | chef/lib/chef/knife/cookbook_upload.rb | 118 | ||||
-rw-r--r-- | chef/spec/unit/cookbook_loader_spec.rb | 2 | ||||
-rw-r--r-- | chef/spec/unit/cookbook_version_spec.rb | 9 |
8 files changed, 149 insertions, 38 deletions
diff --git a/chef-server-api/app/controllers/cookbooks.rb b/chef-server-api/app/controllers/cookbooks.rb index d04eb0ed90..c109dadf87 100644 --- a/chef-server-api/app/controllers/cookbooks.rb +++ b/chef-server-api/app/controllers/cookbooks.rb @@ -119,6 +119,12 @@ class Cookbooks < Application cookbook = params['inflated_object'] end + if cookbook.frozen_version? && params[:force].nil? + raise Conflict, "The cookbook #{cookbook.name} at version #{cookbook.version} is frozen. Use the 'force' option to override." + end + + cookbook.freeze_version if params["inflated_object"].frozen_version? + # ensure that all checksums referred to by the manifest have been uploaded. Chef::CookbookVersion::COOKBOOK_SEGMENTS.each do |segment| next unless cookbook.manifest[segment] diff --git a/chef/lib/chef/cookbook_loader.rb b/chef/lib/chef/cookbook_loader.rb index 56bb9092a5..463dafbd7d 100644 --- a/chef/lib/chef/cookbook_loader.rb +++ b/chef/lib/chef/cookbook_loader.rb @@ -19,6 +19,7 @@ # limitations under the License. require 'chef/config' +require 'chef/exceptions' require 'chef/cookbook/cookbook_version_loader' require 'chef/cookbook_version' require 'chef/cookbook/chefignore' @@ -71,7 +72,7 @@ class Chef if @cookbooks_by_name.has_key?(cookbook.to_sym) @cookbooks_by_name[cookbook.to_sym] else - raise ArgumentError, "Cannot find a cookbook named #{cookbook.to_s}; did you forget to add metadata to a cookbook? (http://wiki.opscode.com/display/chef/Metadata)" + raise Exceptions::CookbookNotFoundInRepo, "Cannot find a cookbook named #{cookbook.to_s}; did you forget to add metadata to a cookbook? (http://wiki.opscode.com/display/chef/Metadata)" end end diff --git a/chef/lib/chef/cookbook_uploader.rb b/chef/lib/chef/cookbook_uploader.rb index 20c21cfd69..72f45c5ad1 100644 --- a/chef/lib/chef/cookbook_uploader.rb +++ b/chef/lib/chef/cookbook_uploader.rb @@ -1,5 +1,5 @@ require 'rest_client' -require 'chef/cookbook_loader' +require 'chef/exceptions' require 'chef/checksum_cache' require 'chef/sandbox' require 'chef/cookbook_version' @@ -11,9 +11,21 @@ class Chef attr_reader :cookbook attr_reader :path + attr_reader :opts - def initialize(cookbook, path) - @cookbook, @path = cookbook, path + # Creates a new CookbookUploader. + # ===Arguments: + # * cookbook::: A Chef::CookbookVersion describing the cookbook to be uploaded + # * path::: A String or Array of Strings representing the base paths to the + # cookbook repositories. + # * opts::: (optional) An options Hash + # ===Options: + # * :force indicates that the uploader should set the force option when + # uploading the cookbook. This allows frozen CookbookVersion + # documents on the server to be overwritten (otherwise a 409 is + # returned by the server) + def initialize(cookbook, path, opts={}) + @cookbook, @path, @opts = cookbook, path, opts end def upload_cookbook @@ -53,6 +65,7 @@ class Chef ) headers = { 'content-type' => 'application/x-binary', 'content-md5' => checksum64, :accept => 'application/json' } headers.merge!(sign_obj.sign(OpenSSL::PKey::RSA.new(rest.signing_key))) + begin RestClient::Resource.new(info['url'], :headers=>headers, :timeout=>1800, :open_timeout=>1800).put(file_contents) rescue RestClient::Exception => e @@ -79,7 +92,7 @@ class Chef end end # files are uploaded, so save the manifest - cookbook.save + opts[:force] ? cookbook.force_save : cookbook.save Chef::Log.info("Upload complete!") end diff --git a/chef/lib/chef/cookbook_version.rb b/chef/lib/chef/cookbook_version.rb index 38f9c323d4..f25e0e5abb 100644 --- a/chef/lib/chef/cookbook_version.rb +++ b/chef/lib/chef/cookbook_version.rb @@ -339,6 +339,7 @@ class Chef # object<Chef::CookbookVersion>:: Duh. :) def initialize(name, couchdb=nil) @name = name + @frozen = false @attribute_filenames = Array.new @definition_filenames = Array.new @template_filenames = Array.new @@ -364,6 +365,17 @@ class Chef metadata.version end + # Indicates if this version is frozen or not. Freezing a coobkook version + # indicates that a new cookbook with the same name and version number + # shoule + def frozen_version? + @frozen + end + + def freeze_version + @frozen = true + end + def version=(new_version) manifest["version"] = new_version metadata.version(new_version) @@ -650,6 +662,7 @@ class Chef def to_hash result = manifest.dup + result['frozen?'] = frozen_version? result['chef_type'] = 'cookbook_version' result["_rev"] = couchdb_rev if couchdb_rev result.to_hash @@ -675,6 +688,7 @@ class Chef cookbook_version.manifest = o # We want the Chef::Cookbook::Metadata object to always be inflated cookbook_version.metadata = Chef::Cookbook::Metadata.from_hash(o["metadata"]) + cookbook_version.freeze_version if o["frozen?"] cookbook_version end @@ -716,12 +730,23 @@ class Chef self.class.chef_server_rest end + # Save this object to the server via the REST api. If there is an existing + # document on the server and it is marked frozen, a + # Net::HTTPServerException will be raised for 409 Conflict. def save chef_server_rest.put_rest("cookbooks/#{name}/#{version}", self) self end alias :create :save + # Adds the `force=true` parameter to the upload. This allows the user to + # overwrite a frozen cookbook (normal #save raises a + # Net::HTTPServerException for 409 Conflict in this case). + def force_save + chef_server_rest.put_rest("cookbooks/#{name}/#{version}?force=true", self) + self + end + def destroy chef_server_rest.delete_rest("cookbooks/#{name}/#{version}") self diff --git a/chef/lib/chef/exceptions.rb b/chef/lib/chef/exceptions.rb index 2cdccbe0b1..58091e34c8 100644 --- a/chef/lib/chef/exceptions.rb +++ b/chef/lib/chef/exceptions.rb @@ -50,6 +50,9 @@ class Chef class RedirectLimitExceeded < RuntimeError; end class AmbiguousRunlistSpecification < ArgumentError; end class CookbookNotFound < RuntimeError; end + # Cookbook loader used to raise an argument error when cookbook not found. + # for back compat, need to raise an error that inherits from ArgumentError + class CookbookNotFoundInRepo < ArgumentError; end class AttributeNotFound < RuntimeError; end class InvalidCommandOption < RuntimeError; end class CommandTimeout < RuntimeError; end diff --git a/chef/lib/chef/knife/cookbook_upload.rb b/chef/lib/chef/knife/cookbook_upload.rb index a2c4b971db..d6f7b3a166 100644 --- a/chef/lib/chef/knife/cookbook_upload.rb +++ b/chef/lib/chef/knife/cookbook_upload.rb @@ -17,6 +17,7 @@ # limitations under the License. # +require 'chef/exceptions' require 'chef/knife' require 'chef/cookbook_loader' require 'chef/cookbook_uploader' @@ -34,57 +35,110 @@ class Chef :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." + def run config[:cookbook_path] ||= Chef::Config[:cookbook_path] - Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::FileSystemFileVendor.new(manifest, config[:cookbook_path]) } + assert_environment_valid! if config[:environment] + version_constraints_to_update = {} - cl = Chef::CookbookLoader.new(config[:cookbook_path]) - - humanize_auth_exceptions do - if config[:all] - cl.each do |cookbook_name, cookbook| - Chef::Log.info("** #{cookbook.name.to_s} **") - Chef::CookbookUploader.new(cookbook, config[:cookbook_path]).upload_cookbook - end - else - if @name_args.length < 1 - show_usage - Chef::Log.fatal("You must specify the --all flag or at least one cookbook name") - exit 1 - end - @name_args.each do |cookbook_name| - if cl.cookbook_exists?(cookbook_name) - Chef::CookbookUploader.new(cl[cookbook_name], config[:cookbook_path]).upload_cookbook - else - Chef::Log.error("Could not find cookbook #{cookbook_name} in your cookbook path, skipping it") - end + if config[:all] + cookbook_repo.each do |cookbook_name, cookbook| + cookbook.freeze_version if config[:freeze] + upload(cookbook) + version_constraints_to_update[cookbook_name] = cookbook.version + end + else + if @name_args.empty? + show_usage + Chef::Log.fatal("You must specify the --all flag or at least one cookbook name") + exit 1 + end + @name_args.each do |cookbook_name| + begin + cookbook = cookbook_repo[cookbook_name] + cookbook.freeze_version if config[:freeze] + upload(cookbook) + version_constraints_to_update[cookbook_name] = cookbook.version + rescue Exceptions::CookbookNotFoundInRepo => e + Log.error("Could not find cookbook #{cookbook_name} in your cookbook path, skipping it") + Log.debug(e) end end end + + 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 ||= Environment.load(config[:environment]) end private - def humanize_auth_exceptions - begin - yield - rescue Net::HTTPServerException => e - case e.response.code - when "401" - Chef::Log.fatal "Request failed due to authentication (#{e}), check your client configuration (username, key)" - exit 18 - else - raise - end + def assert_environment_valid! + environment + rescue Net::HTTPServerException => e + if e.response.code.to_s == "404" + Log.error "The environment #{config[:environment]} does not exist on the server" + Log.debug(e) + exit 1 + else + raise end end + def upload(cookbook) + Chef::Log.info("** #{cookbook.name} **") + Chef::CookbookUploader.new(cookbook, config[:cookbook_path], :force => config[:force]).upload_cookbook + rescue Net::HTTPServerException => e + case e.response.code + when "401" + # The server has good messages for 401s now, so use them: + Log.error Array(Chef::JSONCompat.from_json(e.response.body)["error"]).first + Log.debug(e) + exit 18 + when "409" + Log.error "Version #{cookbook.version} of cookbook #{cookbook.name} is frozen. Use --force to override." + Log.debug(e) + else + raise + end + end end end diff --git a/chef/spec/unit/cookbook_loader_spec.rb b/chef/spec/unit/cookbook_loader_spec.rb index 97c1ccab8f..08e833fa71 100644 --- a/chef/spec/unit/cookbook_loader_spec.rb +++ b/chef/spec/unit/cookbook_loader_spec.rb @@ -32,7 +32,7 @@ describe Chef::CookbookLoader do it "should raise an exception if it cannot find a cookbook with []" do - lambda { @cookbook_loader[:monkeypoop] }.should raise_error(ArgumentError) + lambda { @cookbook_loader[:monkeypoop] }.should raise_error(Chef::Exceptions::CookbookNotFoundInRepo) end it "should allow you to look up available cookbooks with [] and a symbol" do diff --git a/chef/spec/unit/cookbook_version_spec.rb b/chef/spec/unit/cookbook_version_spec.rb index 772fab3061..0f37c66566 100644 --- a/chef/spec/unit/cookbook_version_spec.rb +++ b/chef/spec/unit/cookbook_version_spec.rb @@ -60,6 +60,15 @@ describe Chef::CookbookVersion do @cookbook_version.metadata_filenames.should be_empty end + it "is not frozen" do + @cookbook_version.should_not be_frozen_version + end + + it "can be frozen" do + @cookbook_version.freeze_version + @cookbook_version.should be_frozen_version + end + it "has no couchdb id" do @cookbook_version.couchdb_id.should be_nil end |