chef-zero 1.4.0.alpha-x86-mingw32

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/LICENSE +201 -0
  2. data/README.md +145 -0
  3. data/Rakefile +11 -0
  4. data/bin/chef-zero +43 -0
  5. data/lib/chef_zero.rb +7 -0
  6. data/lib/chef_zero/cookbook_data.rb +223 -0
  7. data/lib/chef_zero/data_normalizer.rb +142 -0
  8. data/lib/chef_zero/data_store/data_already_exists_error.rb +29 -0
  9. data/lib/chef_zero/data_store/data_error.rb +31 -0
  10. data/lib/chef_zero/data_store/data_not_found_error.rb +29 -0
  11. data/lib/chef_zero/data_store/memory_store.rb +167 -0
  12. data/lib/chef_zero/endpoints/actor_endpoint.rb +68 -0
  13. data/lib/chef_zero/endpoints/actors_endpoint.rb +32 -0
  14. data/lib/chef_zero/endpoints/authenticate_user_endpoint.rb +25 -0
  15. data/lib/chef_zero/endpoints/cookbook_endpoint.rb +39 -0
  16. data/lib/chef_zero/endpoints/cookbook_version_endpoint.rb +110 -0
  17. data/lib/chef_zero/endpoints/cookbooks_base.rb +65 -0
  18. data/lib/chef_zero/endpoints/cookbooks_endpoint.rb +19 -0
  19. data/lib/chef_zero/endpoints/data_bag_endpoint.rb +45 -0
  20. data/lib/chef_zero/endpoints/data_bag_item_endpoint.rb +25 -0
  21. data/lib/chef_zero/endpoints/data_bags_endpoint.rb +22 -0
  22. data/lib/chef_zero/endpoints/environment_cookbook_endpoint.rb +24 -0
  23. data/lib/chef_zero/endpoints/environment_cookbook_versions_endpoint.rb +109 -0
  24. data/lib/chef_zero/endpoints/environment_cookbooks_endpoint.rb +22 -0
  25. data/lib/chef_zero/endpoints/environment_endpoint.rb +33 -0
  26. data/lib/chef_zero/endpoints/environment_nodes_endpoint.rb +23 -0
  27. data/lib/chef_zero/endpoints/environment_recipes_endpoint.rb +22 -0
  28. data/lib/chef_zero/endpoints/environment_role_endpoint.rb +36 -0
  29. data/lib/chef_zero/endpoints/file_store_file_endpoint.rb +22 -0
  30. data/lib/chef_zero/endpoints/node_endpoint.rb +17 -0
  31. data/lib/chef_zero/endpoints/not_found_endpoint.rb +11 -0
  32. data/lib/chef_zero/endpoints/principal_endpoint.rb +30 -0
  33. data/lib/chef_zero/endpoints/rest_list_endpoint.rb +40 -0
  34. data/lib/chef_zero/endpoints/rest_object_endpoint.rb +61 -0
  35. data/lib/chef_zero/endpoints/role_endpoint.rb +16 -0
  36. data/lib/chef_zero/endpoints/role_environments_endpoint.rb +14 -0
  37. data/lib/chef_zero/endpoints/sandbox_endpoint.rb +27 -0
  38. data/lib/chef_zero/endpoints/sandboxes_endpoint.rb +51 -0
  39. data/lib/chef_zero/endpoints/search_endpoint.rb +188 -0
  40. data/lib/chef_zero/endpoints/searches_endpoint.rb +18 -0
  41. data/lib/chef_zero/log.rb +7 -0
  42. data/lib/chef_zero/rest_base.rb +133 -0
  43. data/lib/chef_zero/rest_error_response.rb +11 -0
  44. data/lib/chef_zero/rest_request.rb +56 -0
  45. data/lib/chef_zero/rest_router.rb +44 -0
  46. data/lib/chef_zero/rspec.rb +107 -0
  47. data/lib/chef_zero/server.rb +309 -0
  48. data/lib/chef_zero/solr/query/binary_operator.rb +53 -0
  49. data/lib/chef_zero/solr/query/phrase.rb +23 -0
  50. data/lib/chef_zero/solr/query/range_query.rb +34 -0
  51. data/lib/chef_zero/solr/query/regexpable_query.rb +29 -0
  52. data/lib/chef_zero/solr/query/subquery.rb +35 -0
  53. data/lib/chef_zero/solr/query/term.rb +45 -0
  54. data/lib/chef_zero/solr/query/unary_operator.rb +43 -0
  55. data/lib/chef_zero/solr/solr_doc.rb +62 -0
  56. data/lib/chef_zero/solr/solr_parser.rb +193 -0
  57. data/lib/chef_zero/version.rb +3 -0
  58. data/spec/run.rb +25 -0
  59. data/spec/support/pedant.rb +117 -0
  60. data/spec/support/stickywicket.pem +27 -0
  61. metadata +204 -0
@@ -0,0 +1,32 @@
1
+ require 'json'
2
+ require 'chef_zero/endpoints/rest_list_endpoint'
3
+
4
+ module ChefZero
5
+ module Endpoints
6
+ # /clients or /users
7
+ class ActorsEndpoint < RestListEndpoint
8
+ def post(request)
9
+ # First, find out if the user actually posted a public key. If not, make
10
+ # one.
11
+ request_body = JSON.parse(request.body, :create_additions => false)
12
+ public_key = request_body['public_key']
13
+ if !public_key
14
+ private_key, public_key = server.gen_key_pair
15
+ request_body['public_key'] = public_key
16
+ request.body = JSON.pretty_generate(request_body)
17
+ end
18
+
19
+ result = super(request)
20
+ if result[0] == 201
21
+ # If we generated a key, stuff it in the response.
22
+ response = JSON.parse(result[2], :create_additions => false)
23
+ response['private_key'] = private_key if private_key
24
+ response['public_key'] = public_key
25
+ json_response(201, response)
26
+ else
27
+ result
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ require 'json'
2
+ require 'chef_zero/rest_base'
3
+
4
+ module ChefZero
5
+ module Endpoints
6
+ # /authenticate_user
7
+ class AuthenticateUserEndpoint < RestBase
8
+ def post(request)
9
+ request_json = JSON.parse(request.body, :create_additions => false)
10
+ name = request_json['name']
11
+ password = request_json['password']
12
+ begin
13
+ user = data_store.get(['users', name])
14
+ verified = JSON.parse(user, :create_additions => false)['password'] == password
15
+ rescue DataStore::DataNotFoundError
16
+ verified = false
17
+ end
18
+ json_response(200, {
19
+ 'name' => name,
20
+ 'verified' => !!verified
21
+ })
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ require 'chef_zero/endpoints/cookbooks_base'
2
+
3
+ module ChefZero
4
+ module Endpoints
5
+ # /cookbooks/NAME
6
+ class CookbookEndpoint < CookbooksBase
7
+ def get(request)
8
+ filter = request.rest_path[1]
9
+ case filter
10
+ when '_latest'
11
+ result = {}
12
+ filter_cookbooks(all_cookbooks_list, {}, 1) do |name, versions|
13
+ if versions.size > 0
14
+ result[name] = build_uri(request.base_uri, ['cookbooks', name, versions[0]])
15
+ end
16
+ end
17
+ json_response(200, result)
18
+ when '_recipes'
19
+ result = []
20
+ filter_cookbooks(all_cookbooks_list, {}, 1) do |name, versions|
21
+ if versions.size > 0
22
+ cookbook = JSON.parse(get_data(request, ['cookbooks', name, versions[0]]), :create_additions => false)
23
+ result += recipe_names(name, cookbook)
24
+ end
25
+ end
26
+ json_response(200, result.sort)
27
+ else
28
+ cookbook_list = { filter => list_data(request, request.rest_path) }
29
+ json_response(200, format_cookbooks_list(request, cookbook_list))
30
+ end
31
+ end
32
+
33
+ def latest_version(versions)
34
+ sorted = versions.sort_by { |version| Gem::Version.new(version.dup) }
35
+ sorted[-1]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,110 @@
1
+ require 'json'
2
+ require 'chef_zero/endpoints/rest_object_endpoint'
3
+ require 'chef_zero/rest_error_response'
4
+ require 'chef_zero/data_normalizer'
5
+
6
+ module ChefZero
7
+ module Endpoints
8
+ # /cookbooks/NAME/VERSION
9
+ class CookbookVersionEndpoint < RestObjectEndpoint
10
+ def get(request)
11
+ if request.rest_path[2] == "_latest" || request.rest_path[2] == "latest"
12
+ request.rest_path[2] = latest_version(list_data(request, request.rest_path[0..1]))
13
+ end
14
+ super(request)
15
+ end
16
+
17
+ def put(request)
18
+ name = request.rest_path[1]
19
+ version = request.rest_path[2]
20
+ existing_cookbook = get_data(request, request.rest_path, :nil)
21
+
22
+ # Honor frozen
23
+ if existing_cookbook
24
+ existing_cookbook_json = JSON.parse(existing_cookbook, :create_additions => false)
25
+ if existing_cookbook_json['frozen?']
26
+ if request.query_params['force'] != "true"
27
+ raise RestErrorResponse.new(409, "The cookbook #{name} at version #{version} is frozen. Use the 'force' option to override.")
28
+ end
29
+ # For some reason, you are forever unable to modify "frozen?" on a frozen cookbook.
30
+ request_body = JSON.parse(request.body, :create_additions => false)
31
+ if !request_body['frozen?']
32
+ request_body['frozen?'] = true
33
+ request.body = JSON.pretty_generate(request_body)
34
+ end
35
+ end
36
+ end
37
+
38
+ # Set the cookbook
39
+ set_data(request, ['cookbooks', name, version], request.body, :create_dir, :create)
40
+
41
+ # If the cookbook was updated, check for deleted files and clean them up
42
+ if existing_cookbook
43
+ missing_checksums = get_checksums(existing_cookbook) - get_checksums(request.body)
44
+ if missing_checksums.size > 0
45
+ hoover_unused_checksums(missing_checksums)
46
+ end
47
+ end
48
+
49
+ already_json_response(existing_cookbook ? 200 : 201, populate_defaults(request, request.body))
50
+ end
51
+
52
+ def delete(request)
53
+ if request.rest_path[2] == "_latest" || request.rest_path[2] == "latest"
54
+ request.rest_path[2] = latest_version(list_data(request, request.rest_path[0..1]))
55
+ end
56
+
57
+ deleted_cookbook = get_data(request)
58
+
59
+ response = super(request)
60
+ cookbook_name = request.rest_path[1]
61
+ delete_data_dir(request, ['cookbooks', cookbook_name]) if list_data(request, ['cookbooks', cookbook_name]).size == 0
62
+
63
+ # Hoover deleted files, if they exist
64
+ hoover_unused_checksums(get_checksums(deleted_cookbook))
65
+ response
66
+ end
67
+
68
+ def get_checksums(cookbook)
69
+ result = []
70
+ JSON.parse(cookbook, :create_additions => false).each_pair do |key, value|
71
+ if value.is_a?(Array)
72
+ value.each do |file|
73
+ if file.is_a?(Hash) && file.has_key?('checksum')
74
+ result << file['checksum']
75
+ end
76
+ end
77
+ end
78
+ end
79
+ result.uniq
80
+ end
81
+
82
+ private
83
+
84
+ def hoover_unused_checksums(deleted_checksums)
85
+ data_store.list(['cookbooks']).each do |cookbook_name|
86
+ data_store.list(['cookbooks', cookbook_name]).each do |version|
87
+ cookbook = data_store.get(['cookbooks', cookbook_name, version])
88
+ deleted_checksums = deleted_checksums - get_checksums(cookbook)
89
+ end
90
+ end
91
+ deleted_checksums.each do |checksum|
92
+ # There can be a race here if multiple cookbooks are uploading.
93
+ data_store.delete(['file_store', 'checksums', checksum])
94
+ end
95
+ end
96
+
97
+ def populate_defaults(request, response_json)
98
+ # Inject URIs into each cookbook file
99
+ cookbook = JSON.parse(response_json, :create_additions => false)
100
+ cookbook = DataNormalizer.normalize_cookbook(cookbook, request.rest_path[1], request.rest_path[2], request.base_uri, request.method)
101
+ JSON.pretty_generate(cookbook)
102
+ end
103
+
104
+ def latest_version(versions)
105
+ sorted = versions.sort_by { |version| Gem::Version.new(version.dup) }
106
+ sorted[-1]
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,65 @@
1
+ require 'json'
2
+ require 'chef_zero/rest_base'
3
+ require 'chef_zero/data_normalizer'
4
+
5
+ module ChefZero
6
+ module Endpoints
7
+ # Common code for endpoints that return cookbook lists
8
+ class CookbooksBase < RestBase
9
+ def format_cookbooks_list(request, cookbooks_list, constraints = {}, num_versions = nil)
10
+ results = {}
11
+ filter_cookbooks(cookbooks_list, constraints, num_versions) do |name, versions|
12
+ versions_list = versions.map do |version|
13
+ {
14
+ 'url' => build_uri(request.base_uri, ['cookbooks', name, version]),
15
+ 'version' => version
16
+ }
17
+ end
18
+ results[name] = {
19
+ 'url' => build_uri(request.base_uri, ['cookbooks', name]),
20
+ 'versions' => versions_list
21
+ }
22
+ end
23
+ results
24
+ end
25
+
26
+ def all_cookbooks_list
27
+ result = {}
28
+ # Race conditions exist here (if someone deletes while listing). I don't care.
29
+ data_store.list(['cookbooks']).each do |name|
30
+ result[name] = data_store.list(['cookbooks', name])
31
+ end
32
+ result
33
+ end
34
+
35
+ def filter_cookbooks(cookbooks_list, constraints = {}, num_versions = nil)
36
+ cookbooks_list.keys.sort.each do |name|
37
+ constraint = Gem::Requirement.new(constraints[name])
38
+ versions = []
39
+ cookbooks_list[name].sort_by { |version| Gem::Version.new(version.dup) }.reverse.each do |version|
40
+ break if num_versions && versions.size >= num_versions
41
+ if constraint.satisfied_by?(Gem::Version.new(version.dup))
42
+ versions << version
43
+ end
44
+ end
45
+ yield [name, versions]
46
+ end
47
+ end
48
+
49
+ def recipe_names(cookbook_name, cookbook)
50
+ result = []
51
+ if cookbook['recipes']
52
+ cookbook['recipes'].each do |recipe|
53
+ if recipe['path'] == "recipes/#{recipe['name']}" && recipe['name'][-3..-1] == '.rb'
54
+ if recipe['name'] == 'default.rb'
55
+ result << cookbook_name
56
+ end
57
+ result << "#{cookbook_name}::#{recipe['name'][0..-4]}"
58
+ end
59
+ end
60
+ end
61
+ result
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,19 @@
1
+ require 'chef_zero/endpoints/cookbooks_base'
2
+
3
+ module ChefZero
4
+ module Endpoints
5
+ # /cookbooks
6
+ class CookbooksEndpoint < CookbooksBase
7
+ def get(request)
8
+ if request.query_params['num_versions'] == 'all'
9
+ num_versions = nil
10
+ elsif request.query_params['num_versions']
11
+ num_versions = request.query_params['num_versions'].to_i
12
+ else
13
+ num_versions = 1
14
+ end
15
+ json_response(200, format_cookbooks_list(request, all_cookbooks_list, {}, num_versions))
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,45 @@
1
+ require 'json'
2
+ require 'chef_zero/endpoints/rest_list_endpoint'
3
+ require 'chef_zero/endpoints/data_bag_item_endpoint'
4
+ require 'chef_zero/rest_error_response'
5
+
6
+ module ChefZero
7
+ module Endpoints
8
+ # /data/NAME
9
+ class DataBagEndpoint < RestListEndpoint
10
+ def initialize(server)
11
+ super(server, 'id')
12
+ end
13
+
14
+ def post(request)
15
+ key = JSON.parse(request.body, :create_additions => false)[identity_key]
16
+ response = super(request)
17
+ if response[0] == 201
18
+ already_json_response(201, DataBagItemEndpoint::populate_defaults(request, request.body, request.rest_path[1], key))
19
+ else
20
+ response
21
+ end
22
+ end
23
+
24
+ def get_key(contents)
25
+ data_bag_item = JSON.parse(contents, :create_additions => false)
26
+ if data_bag_item['json_class'] == 'Chef::DataBagItem' && data_bag_item['raw_data']
27
+ data_bag_item['raw_data']['id']
28
+ else
29
+ data_bag_item['id']
30
+ end
31
+ end
32
+
33
+ def delete(request)
34
+ key = request.rest_path[1]
35
+ delete_data_dir(request, request.rest_path, :recursive)
36
+ json_response(200, {
37
+ 'chef_type' => 'data_bag',
38
+ 'json_class' => 'Chef::DataBag',
39
+ 'name' => key
40
+ })
41
+ end
42
+ end
43
+ end
44
+ end
45
+
@@ -0,0 +1,25 @@
1
+ require 'json'
2
+ require 'chef_zero/endpoints/rest_object_endpoint'
3
+ require 'chef_zero/endpoints/data_bag_item_endpoint'
4
+ require 'chef_zero/data_normalizer'
5
+
6
+ module ChefZero
7
+ module Endpoints
8
+ # /data/NAME/NAME
9
+ class DataBagItemEndpoint < RestObjectEndpoint
10
+ def initialize(server)
11
+ super(server, 'id')
12
+ end
13
+
14
+ def populate_defaults(request, response_json)
15
+ DataBagItemEndpoint::populate_defaults(request, response_json, request.rest_path[1], request.rest_path[2])
16
+ end
17
+
18
+ def self.populate_defaults(request, response_json, data_bag, data_bag_item)
19
+ response = JSON.parse(response_json, :create_additions => false)
20
+ response = DataNormalizer.normalize_data_bag_item(response, data_bag, data_bag_item, request.method)
21
+ JSON.pretty_generate(response)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ require 'json'
2
+ require 'chef_zero/endpoints/rest_list_endpoint'
3
+
4
+ module ChefZero
5
+ module Endpoints
6
+ # /data
7
+ class DataBagsEndpoint < RestListEndpoint
8
+ def post(request)
9
+ contents = request.body
10
+ name = JSON.parse(contents, :create_additions => false)[identity_key]
11
+ if name.nil?
12
+ error(400, "Must specify '#{identity_key}' in JSON")
13
+ elsif exists_data_dir?(request, ['data', name])
14
+ error(409, "Object already exists")
15
+ else
16
+ data_store.create_dir(['data'], name, :recursive)
17
+ json_response(201, {"uri" => "#{build_uri(request.base_uri, request.rest_path + [name])}"})
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ require 'json'
2
+ require 'chef_zero/endpoints/cookbooks_base'
3
+
4
+ module ChefZero
5
+ module Endpoints
6
+ # /environments/NAME/cookbooks/NAME
7
+ class EnvironmentCookbookEndpoint < CookbooksBase
8
+ def get(request)
9
+ cookbook_name = request.rest_path[3]
10
+ environment = JSON.parse(get_data(request, request.rest_path[0..1]), :create_additions => false)
11
+ constraints = environment['cookbook_versions'] || {}
12
+ cookbook_versions = list_data(request, request.rest_path[2..3])
13
+ if request.query_params['num_versions'] == 'all'
14
+ num_versions = nil
15
+ elsif request.query_params['num_versions']
16
+ num_versions = request.query_params['num_versions'].to_i
17
+ else
18
+ num_versions = nil
19
+ end
20
+ json_response(200, format_cookbooks_list(request, { cookbook_name => cookbook_versions }, constraints, num_versions))
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,109 @@
1
+ require 'json'
2
+ require 'chef_zero/rest_base'
3
+ require 'chef_zero/rest_error_response'
4
+
5
+ module ChefZero
6
+ module Endpoints
7
+ # /environments/NAME/cookbook_versions
8
+ class EnvironmentCookbookVersionsEndpoint < RestBase
9
+ def post(request)
10
+ cookbook_names = list_data(request, ['cookbooks'])
11
+
12
+ # Get the list of cookbooks and versions desired by the runlist
13
+ desired_versions = {}
14
+ run_list = JSON.parse(request.body, :create_additions => false)['run_list']
15
+ run_list.each do |run_list_entry|
16
+ if run_list_entry =~ /(.+)(::.+)?\@(.+)/
17
+ raise RestErrorResponse.new(412, "No such cookbook: #{$1}") if !cookbook_names.include?($1)
18
+ raise RestErrorResponse.new(412, "No such cookbook version for cookbook #{$1}: #{$3}") if !list_data(request, ['cookbooks', $1]).include?($3)
19
+ desired_versions[$1] = [ $3 ]
20
+ else
21
+ desired_cookbook = run_list_entry.split('::')[0]
22
+ raise RestErrorResponse.new(412, "No such cookbook: #{desired_cookbook}") if !cookbook_names.include?(desired_cookbook)
23
+ desired_versions[desired_cookbook] = list_data(request, ['cookbooks', desired_cookbook])
24
+ end
25
+ end
26
+
27
+ # Filter by environment constraints
28
+ environment = JSON.parse(get_data(request, request.rest_path[0..1]), :create_additions => false)
29
+ environment_constraints = environment['cookbook_versions'] || {}
30
+
31
+ desired_versions.each_key do |name|
32
+ desired_versions = filter_by_constraint(desired_versions, name, environment_constraints[name])
33
+ end
34
+
35
+ # Depsolve!
36
+ solved = depsolve(request, desired_versions.keys, desired_versions, environment_constraints)
37
+ if !solved
38
+ return raise RestErrorResponse.new(412, "Unsolvable versions!")
39
+ end
40
+
41
+ result = {}
42
+ solved.each_pair do |name, versions|
43
+ cookbook = JSON.parse(get_data(request, ['cookbooks', name, versions[0]]), :create_additions => false)
44
+ result[name] = DataNormalizer.normalize_cookbook(cookbook, name, versions[0], request.base_uri, 'MIN')
45
+ end
46
+ json_response(200, result)
47
+ end
48
+
49
+ def depsolve(request, unsolved, desired_versions, environment_constraints)
50
+ return nil if desired_versions.values.any? { |versions| versions.empty? }
51
+
52
+ # If everything is already
53
+ solve_for = unsolved[0]
54
+ return desired_versions if !solve_for
55
+
56
+ # Go through each desired version of this cookbook, starting with the latest,
57
+ # until we find one we can solve successfully with
58
+ sort_versions(desired_versions[solve_for]).each do |desired_version|
59
+ new_desired_versions = desired_versions.clone
60
+ new_desired_versions[solve_for] = [ desired_version ]
61
+ new_unsolved = unsolved[1..-1]
62
+
63
+ # Pick this cookbook, and add dependencies
64
+ cookbook_obj = JSON.parse(get_data(request, ['cookbooks', solve_for, desired_version]), :create_additions => false)
65
+ cookbook_metadata = cookbook_obj['metadata'] || {}
66
+ cookbook_dependencies = cookbook_metadata['dependencies'] || {}
67
+ dep_not_found = false
68
+ cookbook_dependencies.each_pair do |dep_name, dep_constraint|
69
+ # If the dep is not already in the list, add it to the list to solve
70
+ # and bring in all environment-allowed cookbook versions to desired_versions
71
+ if !new_desired_versions.has_key?(dep_name)
72
+ new_unsolved = new_unsolved + [dep_name]
73
+ # If the dep is missing, we will try other versions of the cookbook that might not have the bad dep.
74
+ if !exists_data_dir?(request, ['cookbooks', dep_name])
75
+ dep_not_found = true
76
+ break
77
+ end
78
+ new_desired_versions[dep_name] = list_data(request, ['cookbooks', dep_name])
79
+ new_desired_versions = filter_by_constraint(new_desired_versions, dep_name, environment_constraints[dep_name])
80
+ end
81
+ new_desired_versions = filter_by_constraint(new_desired_versions, dep_name, dep_constraint)
82
+ end
83
+
84
+ next if dep_not_found
85
+
86
+ # Depsolve children with this desired version! First solution wins.
87
+ result = depsolve(request, new_unsolved, new_desired_versions, environment_constraints)
88
+ return result if result
89
+ end
90
+ return nil
91
+ end
92
+
93
+ def sort_versions(versions)
94
+ result = versions.sort_by { |version| Gem::Version.new(version.dup) }
95
+ result.reverse
96
+ end
97
+
98
+ def filter_by_constraint(versions, cookbook_name, constraint)
99
+ return versions if !constraint
100
+ constraint = Gem::Requirement.new(constraint)
101
+ new_versions = versions[cookbook_name]
102
+ new_versions = new_versions.select { |version| constraint.satisfied_by?(Gem::Version.new(version.dup)) }
103
+ result = versions.clone
104
+ result[cookbook_name] = new_versions
105
+ result
106
+ end
107
+ end
108
+ end
109
+ end