Module: Chef::CookbookVersionSelector

Defined in:
lib/chef/cookbook_version_selector.rb

Class Method Summary collapse

Class Method Details

.constrain(all_cookbooks, recipe_constraints) ⇒ Object

Return a hash mapping cookbook names to a CookbookVersion object. If there is no solution that satisfies the constraints, the first run list item that caused unsatisfiability is returned.

This is the final version-resolved list of cookbooks for the RunList.

all_cookbooks - a hash mapping cookbook names to an array of available CookbookVersions.

recipe_constraints - an array of hashes describing the expanded run list. Each element is a hash containing keys :name and :version_constraint. The :name component is either the fully-qualified recipe name (e.g. “cookbook1::non_default_recipe”) or just a cookbook name, indicating the default recipe is to be run (e.g. “cookbook1”).



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/chef/cookbook_version_selector.rb', line 84

def self.constrain(all_cookbooks, recipe_constraints)
  dep_graph = create_dependency_graph_from_cookbooks(all_cookbooks)

  # extract cookbook names from (possibly) fully-qualified recipe names
  cookbook_constraints = recipe_constraints.map do |recipe_spec|
    cookbook_name = (recipe_spec[:name][/^(.+)::/, 1] || recipe_spec[:name])
    DepSelector::SolutionConstraint.new(dep_graph.package(cookbook_name),
                                        recipe_spec[:version_constraint])
  end

  # Pass in the list of all available cookbooks (packages) so that
  # DepSelector can distinguish between "no version available for
  # cookbook X" and "no such cookbook X" when an environment
  # filters out all versions for a given cookbook.
  all_packages = all_cookbooks.inject([]) do |acc, (cookbook_name, cookbook_versions)|
    acc << dep_graph.package(cookbook_name)
    acc
  end

  # find a valid assignment of CoookbookVersions. If no valid
  # assignment exists, indicate which run_list_item causes the
  # unsatisfiability and try to hint at what might be wrong.
  soln =
    begin
      DepSelector::Selector.new(dep_graph).find_solution(cookbook_constraints, all_packages)
    rescue DepSelector::Exceptions::InvalidSolutionConstraints => e
      non_existent_cookbooks = e.non_existent_packages.map {|constraint| constraint.package.name}
      cookbooks_with_no_matching_versions = e.constrained_to_no_versions.map {|constraint| constraint.package.name}

      # Spend a whole lot of effort for pluralizing and
      # prettifying the message.
      message = ""
      if non_existent_cookbooks.length > 0
        message += "no such " + (non_existent_cookbooks.length > 1 ? "cookbooks" : "cookbook")
        message += " #{non_existent_cookbooks.join(", ")}"
      end

      if cookbooks_with_no_matching_versions.length > 0
        if message.length > 0
          message += "; "
        end

        message += "no versions match the constraints on " + (cookbooks_with_no_matching_versions.length > 1 ? "cookbooks" : "cookbook")
        message += " #{cookbooks_with_no_matching_versions.join(", ")}"
      end

      message = "Run list contains invalid items: #{message}."

      raise Chef::Exceptions::CookbookVersionSelection::InvalidRunListItems.new(message, non_existent_cookbooks, cookbooks_with_no_matching_versions)
    rescue DepSelector::Exceptions::NoSolutionExists => e
      raise Chef::Exceptions::CookbookVersionSelection::UnsatisfiableRunListItem.new(filter_dep_selector_message(e.message), e.unsatisfiable_solution_constraint, e.disabled_non_existent_packages, e.disabled_most_constrained_packages)
    end


  # map assignment back to CookbookVersion objects
  selected_cookbooks = {}
  soln.each_pair do |cb_name, cb_version|
    # TODO [cw, 2011/2/10]: related to the TODO in
    # create_dependency_graph_from_cookbooks, cbv.version
    # currently returns a String, so we must compare to
    # cb_version.to_s, since it's a for-real Version object.
    selected_cookbooks[cb_name] = all_cookbooks[cb_name].find{|cbv| cbv.version == cb_version.to_s}
  end
  selected_cookbooks
end

.create_dependency_graph_from_cookbooks(all_cookbooks) ⇒ Object

all_cookbooks - a hash mapping cookbook names to an array of available CookbookVersions.

Creates a DependencyGraph from CookbookVersion objects



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/chef/cookbook_version_selector.rb', line 41

def self.create_dependency_graph_from_cookbooks(all_cookbooks)
  dep_graph = DepSelector::DependencyGraph.new

  all_cookbooks.each do |cb_name, cb_versions|
    cb_versions.each do |cb_version|
      cb_version_deps = cb_version..dependencies
      # TODO [cw. 2011/2/10]: CookbookVersion#version returns a
      # String even though we're storing as a DepSelector::Version
      # object underneath. This should be changed so that we
      # return the object and handle proper serialization and
      # de-serialization. For now, I'm just going to create a
      # Version object from the String representation.
      pv = dep_graph.package(cb_name).add_version(Chef::Version.new(cb_version.version))
      cb_version_deps.each_pair do |dep_name, constraint_str|
        # if the dependency is specified as cookbook::recipe,
        # extract the cookbook component
        dep_cb_name = dep_name.split("::").first
        constraint = Chef::VersionConstraint.new(constraint_str)
        pv.dependencies << DepSelector::Dependency.new(dep_graph.package(dep_cb_name), constraint)
      end
    end
  end

  dep_graph
end

.expand_to_cookbook_versions(run_list, environment, couchdb = nil) ⇒ Object

Expands the run_list, constrained to the environment's CookbookVersion constraints.

Returns:

Hash of: name to CookbookVersion


155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/chef/cookbook_version_selector.rb', line 155

def self.expand_to_cookbook_versions(run_list, environment, couchdb=nil)
  # expand any roles in this run_list.
  expanded_run_list = run_list.expand(environment, 'couchdb', :couchdb => couchdb).recipes.with_version_constraints

  cookbooks_for_environment = Chef::Environment.cdb_minimal_filtered_versions(environment, couchdb)
  cookbook_collection = constrain(cookbooks_for_environment, expanded_run_list)
  full_cookbooks = Chef::MinimalCookbookVersion.load_full_versions_of(cookbook_collection.values, couchdb)
  full_cookbooks.inject({}) do |cb_map, cookbook_version|
    cb_map[cookbook_version.name] = cookbook_version
    cb_map
  end
end

.filter_dep_selector_message(message) ⇒ Object

This method replaces verbiage from DepSelector messages with Chef-domain-specific verbiage, such as replacing package with cookbook.

TODO [cw, 2011/2/25]: this is a near-term hack. In the long run, we'll do this better.



28
29
30
31
32
33
34
35
# File 'lib/chef/cookbook_version_selector.rb', line 28

def self.filter_dep_selector_message(message)
  m = message
  m.gsub!("Package", "Cookbook")
  m.gsub!("package", "cookbook")
  m.gsub!("Solution constraint", "Run list item")
  m.gsub!("solution constraint", "run list item")
  m
end