Skip to content

Commit fd78aaf

Browse files
sdelanoSeth Falcon
authored andcommitted
restrict cookbook loading to environment versions
1 parent e41f6cd commit fd78aaf

1 file changed

Lines changed: 35 additions & 93 deletions

File tree

chef-server-api/app/controllers/nodes.rb

Lines changed: 35 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,6 @@ class Nodes < Application
2727
before :authenticate_every
2828
before :admin_or_requesting_node, :only => [ :update, :destroy, :cookbooks ]
2929

30-
CMP = {
31-
"<<" => lambda { |v, r| v < r },
32-
"<=" => lambda { |v, r| v <= r },
33-
"=" => lambda { |v, r| v == r },
34-
">=" => lambda { |v, r| v >= r },
35-
">>" => lambda { |v, r| v > r }
36-
}
37-
38-
qcmp = CMP.keys.map { |k| Regexp.quote k }.join "|"
39-
PATTERN = /\A\s*(#{qcmp})?\s*(#{Chef::Cookbook::Metadata::Version::PATTERN})\s*\z/
40-
4130
def index
4231
@node_list = Chef::Node.cdb_list
4332
display(@node_list.inject({}) do |r,n|
@@ -103,52 +92,12 @@ def cookbooks
10392

10493
private
10594

106-
def satisfy(cookbook, req=nil)
107-
r = req.to_s
108-
begin
109-
versions = Chef::CookbookVersion.cdb_by_name(cookbook)[cookbook].sort
110-
rescue NoMethodError
111-
raise PreconditionFailed, "cookbook #{cookbook} does not exist"
112-
end
113-
if r.nil? or r.empty? or r == "latest"
114-
versions
115-
elsif r =~ PATTERN
116-
comp = $1 || "="
117-
ver = Chef::Cookbook::Metadata::Version.new $2
118-
versions.select { |v| CMP[comp].call Chef::Cookbook::Metadata::Version.new(v), ver}
119-
else
120-
raise ArgumentError, "Unrecognized dependency specification: #{r}"
121-
end
122-
end
123-
124-
def satisfy_all(cookbook, reqs=[])
125-
vers = Array.new
126-
127-
if reqs.empty?
128-
return Chef::CookbookVersion.cdb_load(cookbook, satisfy(cookbook).last)
129-
end
130-
131-
reqs.each do |pat|
132-
v = satisfy(cookbook, pat)
133-
raise ArgumentError, "Can't satisfy dependency #{pat} for cookbook #{cookbook}" if v.empty?
134-
if vers.empty?
135-
vers = v
136-
else
137-
vers = vers & v
138-
raise ArgumentError, "Conflicting dependencies for #{cookbook}" if vers.empty?
139-
end
140-
end
141-
Chef::CookbookVersion.cdb_load(cookbook, vers.last)
142-
end
143-
14495
def load_all_files
14596
all_cookbooks = Chef::Environment.cdb_load_filtered_cookbook_versions(@node.chef_environment)
14697

14798
included_cookbooks = cookbooks_for_node(all_cookbooks)
14899
nodes_cookbooks = Hash.new
149-
included_cookbooks.each_pair do |cookbook_name, versions|
150-
cookbook = satisfy_all(cookbook_name, versions)
151-
100+
included_cookbooks.each do |cookbook_name, cookbook|
152101
nodes_cookbooks[cookbook_name.to_s] = cookbook.generate_manifest_with_urls{|opts| absolute_url(:cookbook_file, opts) }
153102
end
154103

@@ -158,12 +107,17 @@ def load_all_files
158107
# returns name -> CookbookVersion for all cookbooks included on the given node.
159108
def cookbooks_for_node(all_cookbooks)
160109
# expand returns a RunListExpansion which contains recipes, default and override attrs [cb]
161-
items = @node.run_list.expand('couchdb').run_list_items
110+
# TODO: check for this on the client side before we make the http request [stephen 9/1/10]
111+
begin
112+
recipes = @node.run_list.expand('couchdb').recipes.with_versions
113+
rescue Chef::Exceptions::RecipeVersionConflict => e
114+
raise PreconditionFailed, "#Conflict: #{e.message}"
115+
end
162116

163117
# TODO: make cookbook loading respect environment's versions [stephen 8/25/10]
164118
# walk run list and accumulate included dependencies
165-
items.select{|i| i.recipe?}.inject({}) do |included_cookbooks, rli|
166-
expand_cookbook_deps(included_cookbooks, rli)
119+
recipes.inject({}) do |included_cookbooks, recipe|
120+
expand_cookbook_deps(included_cookbooks, all_cookbooks, recipe, "Run list")
167121
included_cookbooks
168122
end
169123
end
@@ -172,47 +126,35 @@ def cookbooks_for_node(all_cookbooks)
172126
# included_cookbooks == hash of name -> CookbookVersion, which is used for returning
173127
# result as well as for tracking which cookbooks we've already
174128
# recursed into
175-
# all_cookbooks == hash of name -> CookbookVersion, all cookbooks available
176-
# run_list_items == name of cookbook to include
177-
def expand_cookbook_deps(included_cookbooks, run_list_item, constraints = nil)
178-
# determine the run list item's parent cookbook, which might be run_list_item in the default case
179-
version = "latest"
180-
181-
# Fortunately or otherwise we need to deal with both RunListItem objects
182-
# and bare strings. strings will come to us via the dependency solver, and
183-
# I don't think it's valuable to bash them into an RLI just to avoid this.
184-
if run_list_item.kind_of? Chef::RunList::RunListItem
185-
cookbook_name = (run_list_item.name[/^(.+)::/, 1] || run_list_item.name)
186-
version = run_list_item.version unless run_list_item.version.nil?
129+
# all_cookbooks == hash of name -> [ CookbookVersion ... ] , all cookbooks available, sorted by version number
130+
# recipe == hash of :name => recipe_name, :version => recipe_version to include
131+
# parent_name == the name of the parent cookbook (or run_list), for reporting broken dependencies
132+
def expand_cookbook_deps(included_cookbooks, all_cookbooks, recipe, parent_name)
133+
# determine the recipe's parent cookbook, which might be the recipe name in the default case
134+
cookbook_name = (recipe[:name][/^(.+)::/, 1] || recipe[:name])
135+
version = recipe[:version] ? Gem::Version.new(recipe[:version]) : nil
136+
Chef::Log.debug "Node requires #{cookbook_name} at version #{version.to_s}"
137+
138+
# detect the correct cookbook version from the list of available cookbook versions
139+
cookbook = version ? all_cookbooks[cookbook_name].detect {|cb| Gem::Version.new(cb.version) == version} : all_cookbooks[cookbook_name].last
140+
raise PreconditionFailed, "#{parent_name} depends on cookbook #{cookbook_name} #{version.to_s}, which is not available to this node" unless cookbook
141+
142+
# we can't load more than one version of the same cookbook
143+
if included_cookbooks[cookbook_name]
144+
a = Gem::Version.new(included_cookbooks[cookbook_name].version)
145+
b = Gem::Version.new(cookbook.version)
146+
raise PreconditionFailed, "Conflict: Node requires cookbook #{cookbook_name} at versions #{a.to_s} and #{b.to_s}" if a != b
187147
else
188-
cookbook_name = (run_list_item[/^(.+)::/, 1] || run_list_item.to_s)
189-
end
190-
191-
Chef::Log.debug("Node requires #{cookbook_name} at #{version}")
192-
Chef::Log.debug("Requirement constrained by #{constraints}") if constraints
193-
194-
constraints ||= version
195-
196-
# include its dependencies
197-
included_cookbooks[cookbook_name] ||= Array.new
198-
included_cookbooks[cookbook_name] << constraints
199-
200-
cb = satisfy_all(cookbook_name, constraints)
201-
202-
if !cb
203-
return false
204-
# NOTE [dan/cw] We don't think changing this to an exception breaks stuff.
205-
# Chef::Log.warn "#{__FILE__}:#{__LINE__}: in expand_cookbook_deps, cookbook/role #{cookbook_name} could not be found, ignoring it in cookbook expansion"
206-
# return included_cookbooks
148+
included_cookbooks[cookbook_name] = cookbook
207149
end
208150

209-
cb.metadata.dependencies.each do |depname, dep_version_constraints|
210-
unless expand_cookbook_deps(included_cookbooks, depname, dep_version_constraints)
211-
raise PreconditionFailed, "cookbook #{cookbook_name} depends on cookbook #{depname}, but #{depname} does not exist"
212-
213-
end
151+
# TODO:
152+
# In the past, we have ignored the version constraints from dependency metadata.
153+
# We will continue to do so for the time being, until the Gem::Version
154+
# sytax for the environments feature is replaced with something more permanent
155+
# [stephen 9/1/10]
156+
cookbook.metadata.dependencies.each do |dependency_name, dependency_version_constraints|
157+
expand_cookbook_deps(included_cookbooks, all_cookbooks, dependency_name, "Cookbook #{cookbook_name}")
214158
end
215-
true
216159
end
217-
218160
end

0 commit comments

Comments
 (0)