Permalink
Browse files

Merge branch 'sd/ruby-depsolver-feature'

  • Loading branch information...
sdelano committed Aug 22, 2013
2 parents f6e9f77 + 1dc921e commit a3133ced037d1e508ff18723ad9a6f2b94dea1ea
@@ -3,7 +3,6 @@ DEPS = $(CURDIR)/deps
DIALYZER_OPTS = -Wunderspecs
DIALYZER_DEPS = deps/chef_authn/ebin \
deps/depsolver/ebin \
deps/ej/ebin \
deps/jiffy/ebin \
deps/ibrowse/ebin \
@@ -53,13 +52,16 @@ compile: $(DEPS)
$(DEPS):
@rebar get-deps
bundle:
@cd priv/depselector_rb; bundle install
# Full clean and removal of all deps. Remove deps first to avoid
# wasted effort of cleaning deps before nuking them.
distclean:
@rm -rf deps $(DEPS_PLT)
@rm -rf deps $(DEPS_PLT) priv/depselector_rb/.bundle
@rebar clean
eunit:
eunit: compile bundle
@rebar skip_deps=true eunit
test: eunit
@@ -0,0 +1,5 @@
source 'https://rubygems.org'
gem 'dep_selector', :git => 'git://github.com/opscode/dep-selector', :branch => 'master'
gem 'erlectricity', :git => 'git://github.com/sdelano/erlectricity', :branch => 'master'
gem 'uuidtools'
@@ -0,0 +1,26 @@
GIT
remote: git://github.com/opscode/dep-selector
revision: 9f97b663e923700070e8fb08f6b94629147a152d
branch: master
specs:
dep_selector (0.1.0)
GIT
remote: git://github.com/sdelano/erlectricity
revision: b8b627fbc4b82cbfe2807ab62733ba0a65d0c209
branch: master
specs:
erlectricity (1.1.1)
GEM
remote: https://rubygems.org/
specs:
uuidtools (2.1.4)
PLATFORMS
ruby
DEPENDENCIES
dep_selector!
erlectricity!
uuidtools
@@ -0,0 +1,127 @@
# ensure that the Gemfile is in the cwd
Dir.chdir(File.dirname(__FILE__))
require 'rubygems'
require 'bundler/setup'
require 'dep_selector'
require 'erlectricity'
def translate_constraint(constraint)
case constraint
when Symbol
case constraint
when :gt then ">"
when :gte then ">="
when :lt then "<"
when :lte then "<="
when :eq then "="
when :pes then "~>"
else constraint.to_s
end
when NilClass
"="
else
constraint
end
end
def constraint_to_str(constraint, constraint_version)
return nil unless constraint_version
"#{translate_constraint(constraint)} #{constraint_version}"
end
receive do |m|
m.when([:solve, Erl.hash]) do |data|
# create dependency graph from cookbooks
graph = DepSelector::DependencyGraph.new
env_constraints = data[:environment_constraints].inject({}) do |acc, env_constraint|
name, version, constraint = env_constraint
acc[name] = DepSelector::VersionConstraint.new(constraint_to_str(constraint, version))
acc
end
all_versions = []
data[:all_versions].each do | vsn|
name, version_constraints = vsn
version_constraints.each do |version_constraint| # todo: constraints become an array in ruby
# due to the erlectricity conversion from
# tuples
version, constraints = version_constraint
# filter versions based on environment constraints
env_constraint = env_constraints[name]
if (!env_constraint || env_constraint.include?(DepSelector::Version.new(version)))
package_version = graph.package(name).add_version(DepSelector::Version.new(version))
constraints.each do |package_constraint|
constraint_name, constraint_version, constraint = package_constraint
version_constraint = DepSelector::VersionConstraint.new(constraint_to_str(constraint, constraint_version))
dependency = DepSelector::Dependency.new(graph.package(constraint_name), version_constraint)
package_version.dependencies << dependency
end
end
end
# regardless of filter, add package reference to all_packages
all_versions << graph.package(name)
end
run_list = data[:run_list].map do |run_list_item|
item_name, item_constraint_version, item_constraint = run_list_item
version_constraint = DepSelector::VersionConstraint.new(constraint_to_str(item_constraint,
item_constraint_version))
DepSelector::SolutionConstraint.new(graph.package(item_name), version_constraint)
end
timeout_ms = data[:timeout_ms]
selector = DepSelector::Selector.new(graph, (timeout_ms / 1000.0))
answer = begin
solution = selector.find_solution(run_list, all_versions)
packages = Erl::List.new
solution.each do |package, v|
packages << [package, [v.major, v.minor, v.patch]]
end
[:ok, packages]
rescue DepSelector::Exceptions::InvalidSolutionConstraints => e
non_existent_cookbooks = e.non_existent_packages.inject(Erl::List.new) do |list, constraint|
list << constraint.package.name
end
constrained_to_no_versions = e.constrained_to_no_versions.inject(Erl::List.new) do |list, constraint|
list << constraint.to_s
end
error_detail = Erl::List.new([[:non_existent_cookbooks, non_existent_cookbooks],
[:constraints_not_met, constrained_to_no_versions]])
[:error, :invalid_constraints, error_detail]
rescue DepSelector::Exceptions::NoSolutionExists => e
most_constrained_cookbooks = e.disabled_most_constrained_packages.inject(Erl::List.new) do |list, package|
# WTF: this is the reported error format but I can't find this anywhere in the ruby code
list << "#{package.name} = #{package.versions.first.to_s}"
end
non_existent_cookbooks = e.disabled_non_existent_packages.inject(Erl::List.new) do |list, package|
list << package.name
end
error_detail = Erl::List.new([[:message, e.message],
[:unsatisfiable_run_list_item, e.unsatisfiable_solution_constraint.to_s],
[:non_existent_cookbooks, non_existent_cookbooks],
[:most_constrained_cookbooks, most_constrained_cookbooks]])
[:error, :no_solution, error_detail]
rescue DepSelector::Exceptions::TimeBoundExceeded,
DepSelector::Exceptions::TimeBoundExceededNoSolution => e
# While dep_selector differentiates between the two solutions, the opscode-chef
# API returns the same error regardless of the timeout type. We'll swallow the
# difference here and return a unified timeout to erchef
[:error, :resolution_timeout]
end
m.send!(answer)
m.receive_loop
end
end
@@ -14,12 +14,12 @@
{git, "git://github.com/opscode/ibrowse.git", {tag, "v4.0.1.1"}}},
{mini_s3, ".*",
{git, "git://github.com/opscode/mini_s3.git", {branch, "master"}}},
{depsolver, ".*",
{git, "git://github.com/opscode/depsolver.git", {branch, "master"}}},
{chef_authn, ".*",
{git, "git://github.com/opscode/chef_authn.git", {branch, "master"}}},
{opscoderl_folsom, ".*",
{git, "git://github.com/opscode/opscoderl_folsom.git", {branch, "master"}}}
{git, "git://github.com/opscode/opscoderl_folsom.git", {branch, "master"}}},
{pooler, ".*",
{git, "git://github.com/seth/pooler.git", {tag, "1.0.0"}}}
]}.
{cover_enabled, true}.
@@ -45,6 +45,37 @@
-define(DEFAULT_DEPSOLVER_TIMEOUT, 2000).
-export_type([constraint/0,
dependency_set/0,
pkg/0]).
%%============================================================================
%% Types
%%============================================================================
-type pkg_name() :: binary() | atom().
-type pkg() :: {pkg_name(), vsn()}.
-type raw_vsn() :: ec_semver:any_version().
-type vsn() :: 'NO_VSN'
| ec_semver:semver().
-type constraint_op() ::
'=' | gte | '>=' | lte | '<='
| gt | '>' | lt | '<' | pes | '~>' | between.
-type raw_constraint() :: pkg_name()
| {pkg_name(), raw_vsn()}
| {pkg_name(), raw_vsn(), constraint_op()}
| {pkg_name(), raw_vsn(), vsn(), between}.
-type constraint() :: pkg_name()
| {pkg_name(), vsn()}
| {pkg_name(), vsn(), constraint_op()}
| {pkg_name(), vsn(), vsn(), between}.
-type vsn_constraint() :: {raw_vsn(), [raw_constraint()]}.
-type dependency_set() :: {pkg_name(), [vsn_constraint()]}.
%% @doc Convert a binary JSON string representing a Chef runlist into an
%% EJson-encoded Erlang data structure.
%% @end
@@ -72,12 +103,11 @@ validate_body(Body) ->
Bad -> throw(Bad)
end.
-spec solve_dependencies(AllVersions :: [depsolver:dependency_set()],
EnvConstraints :: [depsolver:constraint()],
-spec solve_dependencies(AllVersions :: [dependency_set()],
EnvConstraints :: [constraint()],
Cookbooks :: [Name::binary() |
{Name::binary(), Version::binary()}]) ->
{ok, [ versioned_cookbook()]} | {error, term()}.
{ok, [ versioned_cookbook()]} | {error, term()}.
%% @doc Main entry point into the depsolver. It is supplied with a dependency_set()
%% containing all the cookbook versions and their dependencies that are in the database
@@ -88,41 +118,10 @@ validate_body(Body) ->
solve_dependencies(_AllVersions, _EnvConstraints, []) ->
{ok, []};
solve_dependencies(AllVersions, EnvConstraints, Cookbooks) ->
%% We apply the environment cookbook version constraints as a pre-filter, removing
%% cookbook versions that don't satisfy early. This makes for a smaller graph and an
%% easier problem to solve. However, when cookbooks are filtered out due to the
%% environment, the solver is unable to backtrack and provide extra error detail. With
%% this approach, the "world" of cookbooks conforms to what the user will see from
%% listing cookbooks within an environment.
{ok, FilteredVersions} =
folsom_time(depsolver, filter_packages_with_deps,
fun() ->
depsolver:filter_packages_with_deps(AllVersions,
EnvConstraints)
end),
Graph = folsom_time(depsolver, add_packages,
fun() ->
depsolver:add_packages(depsolver:new_graph(),
FilteredVersions)
end),
Result = folsom_time(depsolver, solve,
fun() ->
depsolver:solve(Graph, Cookbooks, depsolver_timeout())
end),
sanitize_semver(Result).
%% @doc The depsolver module (as of version 0.1.0) supports semver and returns a version
%% structure as `{Name, {{1, 2, 3}, {Alpha, Build}}}'. Chef does not currently support
%% semver style versions for cookbooks. For successful solve results, we simplify the
%% return. Error returns will contain version data (with semver details). These are left in
%% place for two reasons: 1) the error structures are not as simple to sanitize; 2) the
%% depsolver_culprits module is used to format the error returns and it is expecting data in
%% this format.
sanitize_semver({ok, WithSemver}) ->
XYZOnly = [ {Name, XYZVersion} || {Name, {XYZVersion, _SemVer}} <- WithSemver ],
{ok, XYZOnly};
sanitize_semver(Error) ->
Error.
chef_depsolver_worker:solve_dependencies(AllVersions,
EnvConstraints,
Cookbooks,
depsolver_timeout()).
depsolver_timeout() ->
case application:get_env(chef_objects, depsolver_timeout) of
Oops, something went wrong.

0 comments on commit a3133ce

Please sign in to comment.