Skip to content

Commit

Permalink
[CHEF-2076] Make CookbookVersions freezable
Browse files Browse the repository at this point in the history
  • Loading branch information
danielsdeleo committed Mar 25, 2011
1 parent 0a53d6d commit 82e0a8a
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 38 deletions.
6 changes: 6 additions & 0 deletions chef-server-api/app/controllers/cookbooks.rb
Expand Up @@ -119,6 +119,12 @@ def update
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]
Expand Down
3 changes: 2 additions & 1 deletion chef/lib/chef/cookbook_loader.rb
Expand Up @@ -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'
Expand Down Expand Up @@ -71,7 +72,7 @@ def [](cookbook)
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

Expand Down
21 changes: 17 additions & 4 deletions 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'
Expand All @@ -11,9 +11,21 @@ class CookbookUploader

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
Expand Down Expand Up @@ -53,6 +65,7 @@ def upload_cookbook
)
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
Expand All @@ -79,7 +92,7 @@ def upload_cookbook
end
end
# files are uploaded, so save the manifest
cookbook.save
opts[:force] ? cookbook.force_save : cookbook.save
Chef::Log.info("Upload complete!")
end

Expand Down
25 changes: 25 additions & 0 deletions chef/lib/chef/cookbook_version.rb
Expand Up @@ -339,6 +339,7 @@ def self.cleanup_file_cache
# 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
Expand All @@ -364,6 +365,17 @@ def version
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)
Expand Down Expand Up @@ -650,6 +662,7 @@ def preferences_for_path(node, segment, path)

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
Expand All @@ -675,6 +688,7 @@ def self.json_create(o)
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

Expand Down Expand Up @@ -716,12 +730,23 @@ def chef_server_rest
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
Expand Down
3 changes: 3 additions & 0 deletions chef/lib/chef/exceptions.rb
Expand Up @@ -50,6 +50,9 @@ class ConfigurationError < ArgumentError; end
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
Expand Down
118 changes: 86 additions & 32 deletions chef/lib/chef/knife/cookbook_upload.rb
Expand Up @@ -17,6 +17,7 @@
# limitations under the License.
#

require 'chef/exceptions'
require 'chef/knife'
require 'chef/cookbook_loader'
require 'chef/cookbook_uploader'
Expand All @@ -34,57 +35,110 @@ class CookbookUpload < Knife
: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
Expand Down
2 changes: 1 addition & 1 deletion chef/spec/unit/cookbook_loader_spec.rb
Expand Up @@ -32,7 +32,7 @@


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
Expand Down
9 changes: 9 additions & 0 deletions chef/spec/unit/cookbook_version_spec.rb
Expand Up @@ -60,6 +60,15 @@
@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
Expand Down

0 comments on commit 82e0a8a

Please sign in to comment.