chef-zero 0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. data/LICENSE +201 -0
  2. data/README.rdoc +79 -0
  3. data/Rakefile +19 -0
  4. data/bin/chef-zero +40 -0
  5. data/lib/chef_zero.rb +5 -0
  6. data/lib/chef_zero/cookbook_data.rb +110 -0
  7. data/lib/chef_zero/data_normalizer.rb +129 -0
  8. data/lib/chef_zero/endpoints/actor_endpoint.rb +68 -0
  9. data/lib/chef_zero/endpoints/actors_endpoint.rb +32 -0
  10. data/lib/chef_zero/endpoints/authenticate_user_endpoint.rb +21 -0
  11. data/lib/chef_zero/endpoints/cookbook_endpoint.rb +39 -0
  12. data/lib/chef_zero/endpoints/cookbook_version_endpoint.rb +106 -0
  13. data/lib/chef_zero/endpoints/cookbooks_base.rb +59 -0
  14. data/lib/chef_zero/endpoints/cookbooks_endpoint.rb +12 -0
  15. data/lib/chef_zero/endpoints/data_bag_endpoint.rb +50 -0
  16. data/lib/chef_zero/endpoints/data_bag_item_endpoint.rb +25 -0
  17. data/lib/chef_zero/endpoints/data_bags_endpoint.rb +21 -0
  18. data/lib/chef_zero/endpoints/environment_cookbook_endpoint.rb +24 -0
  19. data/lib/chef_zero/endpoints/environment_cookbook_versions_endpoint.rb +114 -0
  20. data/lib/chef_zero/endpoints/environment_cookbooks_endpoint.rb +22 -0
  21. data/lib/chef_zero/endpoints/environment_endpoint.rb +33 -0
  22. data/lib/chef_zero/endpoints/environment_nodes_endpoint.rb +23 -0
  23. data/lib/chef_zero/endpoints/environment_recipes_endpoint.rb +22 -0
  24. data/lib/chef_zero/endpoints/environment_role_endpoint.rb +35 -0
  25. data/lib/chef_zero/endpoints/file_store_file_endpoint.rb +22 -0
  26. data/lib/chef_zero/endpoints/node_endpoint.rb +17 -0
  27. data/lib/chef_zero/endpoints/not_found_endpoint.rb +9 -0
  28. data/lib/chef_zero/endpoints/principal_endpoint.rb +30 -0
  29. data/lib/chef_zero/endpoints/rest_list_endpoint.rb +41 -0
  30. data/lib/chef_zero/endpoints/rest_object_endpoint.rb +65 -0
  31. data/lib/chef_zero/endpoints/role_endpoint.rb +16 -0
  32. data/lib/chef_zero/endpoints/role_environments_endpoint.rb +14 -0
  33. data/lib/chef_zero/endpoints/sandbox_endpoint.rb +22 -0
  34. data/lib/chef_zero/endpoints/sandboxes_endpoint.rb +44 -0
  35. data/lib/chef_zero/endpoints/search_endpoint.rb +139 -0
  36. data/lib/chef_zero/endpoints/searches_endpoint.rb +18 -0
  37. data/lib/chef_zero/rest_base.rb +82 -0
  38. data/lib/chef_zero/rest_error_response.rb +11 -0
  39. data/lib/chef_zero/rest_request.rb +42 -0
  40. data/lib/chef_zero/router.rb +26 -0
  41. data/lib/chef_zero/server.rb +255 -0
  42. data/lib/chef_zero/solr/query/binary_operator.rb +53 -0
  43. data/lib/chef_zero/solr/query/phrase.rb +23 -0
  44. data/lib/chef_zero/solr/query/range_query.rb +34 -0
  45. data/lib/chef_zero/solr/query/regexpable_query.rb +29 -0
  46. data/lib/chef_zero/solr/query/subquery.rb +35 -0
  47. data/lib/chef_zero/solr/query/term.rb +45 -0
  48. data/lib/chef_zero/solr/query/unary_operator.rb +43 -0
  49. data/lib/chef_zero/solr/solr_doc.rb +62 -0
  50. data/lib/chef_zero/solr/solr_parser.rb +194 -0
  51. data/lib/chef_zero/version.rb +3 -0
  52. metadata +132 -0
@@ -0,0 +1,129 @@
1
+ require 'chef_zero'
2
+ require 'chef_zero/rest_base'
3
+
4
+ module ChefZero
5
+ class DataNormalizer
6
+ def self.normalize_client(client, name)
7
+ client['name'] ||= name
8
+ client['admin'] ||= false
9
+ client['public_key'] ||= PUBLIC_KEY
10
+ client['validator'] ||= false
11
+ client['json_class'] ||= "Chef::ApiClient"
12
+ client['chef_type'] ||= "client"
13
+ client
14
+ end
15
+
16
+ def self.normalize_user(user, name)
17
+ user['name'] ||= name
18
+ user['admin'] ||= false
19
+ user['public_key'] ||= PUBLIC_KEY
20
+ user
21
+ end
22
+
23
+ def self.normalize_data_bag_item(data_bag_item, data_bag_name, id, method)
24
+ if method == 'DELETE'
25
+ # TODO SERIOUSLY, WHO DOES THIS MANY EXCEPTIONS IN THEIR INTERFACE
26
+ if !(data_bag_item['json_class'] == 'Chef::DataBagItem' && data_bag_item['raw_data'])
27
+ data_bag_item['id'] ||= id
28
+ data_bag_item = { 'raw_data' => data_bag_item }
29
+ data_bag_item['chef_type'] ||= 'data_bag_item'
30
+ data_bag_item['json_class'] ||= 'Chef::DataBagItem'
31
+ data_bag_item['data_bag'] ||= data_bag_name
32
+ data_bag_item['name'] ||= "data_bag_item_#{data_bag_name}_#{id}"
33
+ end
34
+ else
35
+ # If it's not already wrapped with raw_data, wrap it.
36
+ if data_bag_item['json_class'] == 'Chef::DataBagItem' && data_bag_item['raw_data']
37
+ data_bag_item = data_bag_item['raw_data']
38
+ end
39
+ # Argh. We don't do this on GET, but we do on PUT and POST????
40
+ if %w(PUT POST).include?(method)
41
+ data_bag_item['chef_type'] ||= 'data_bag_item'
42
+ data_bag_item['data_bag'] ||= data_bag_name
43
+ end
44
+ data_bag_item['id'] ||= id
45
+ end
46
+ data_bag_item
47
+ end
48
+
49
+ def self.normalize_environment(environment, name)
50
+ environment['name'] ||= name
51
+ environment['description'] ||= ''
52
+ environment['cookbook_versions'] ||= {}
53
+ environment['json_class'] ||= "Chef::Environment"
54
+ environment['chef_type'] ||= "environment"
55
+ environment['default_attributes'] ||= {}
56
+ environment['override_attributes'] ||= {}
57
+ environment
58
+ end
59
+
60
+ def self.normalize_cookbook(cookbook, name, version, base_uri, method)
61
+ # TODO I feel dirty
62
+ if method != 'PUT'
63
+ cookbook.each_pair do |key, value|
64
+ if value.is_a?(Array)
65
+ value.each do |file|
66
+ if file.is_a?(Hash) && file.has_key?('checksum')
67
+ file['url'] ||= RestBase::build_uri(base_uri, ['file_store', file['checksum']])
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ cookbook['name'] ||= "#{name}-#{version}"
74
+ # TODO this feels wrong, but the real chef server doesn't expand this default
75
+ # cookbook['version'] ||= version
76
+ cookbook['cookbook_name'] ||= name
77
+ cookbook['json_class'] ||= 'Chef::CookbookVersion'
78
+ cookbook['chef_type'] ||= 'cookbook_version'
79
+ cookbook['frozen?'] ||= false
80
+ cookbook['metadata'] ||= {}
81
+ cookbook['metadata']['version'] ||= version
82
+ cookbook['metadata']['name'] ||= name
83
+ cookbook
84
+ end
85
+
86
+ def self.normalize_node(node, name)
87
+ node['name'] ||= name
88
+ node['json_class'] ||= 'Chef::Node'
89
+ node['chef_type'] ||= 'node'
90
+ node['chef_environment'] ||= '_default'
91
+ node['override'] ||= {}
92
+ node['normal'] ||= {}
93
+ node['default'] ||= {}
94
+ node['automatic'] ||= {}
95
+ node['run_list'] ||= []
96
+ node['run_list'] = normalize_run_list(node['run_list'])
97
+ node
98
+ end
99
+
100
+ def self.normalize_role(role, name)
101
+ role['name'] ||= name
102
+ role['description'] ||= ''
103
+ role['json_class'] ||= 'Chef::Role'
104
+ role['chef_type'] ||= 'role'
105
+ role['default_attributes'] ||= {}
106
+ role['override_attributes'] ||= {}
107
+ role['run_list'] ||= []
108
+ role['run_list'] = normalize_run_list(role['run_list'])
109
+ role['env_run_lists'] ||= {}
110
+ role['env_run_lists'].each_pair do |env, run_list|
111
+ role['env_run_lists'][env] = normalize_run_list(run_list)
112
+ end
113
+ role
114
+ end
115
+
116
+ def self.normalize_run_list(run_list)
117
+ run_list.map{|item|
118
+ case item
119
+ when /^recipe\[.*\]$/
120
+ item # explicit recipe
121
+ when /^role\[.*\]$/
122
+ item # explicit role
123
+ else
124
+ "recipe[#{item}]"
125
+ end
126
+ }.uniq
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,68 @@
1
+ require 'json'
2
+ require 'chef_zero/endpoints/rest_object_endpoint'
3
+ require 'chef_zero/data_normalizer'
4
+
5
+ module ChefZero
6
+ module Endpoints
7
+ # /clients/* and /users/*
8
+ class ActorEndpoint < RestObjectEndpoint
9
+ def put(request)
10
+ # Find out if we're updating the public key.
11
+ request_body = JSON.parse(request.body, :create_additions => false)
12
+ if request_body['public_key'].nil?
13
+ # If public_key is null, then don't overwrite it. Weird patchiness.
14
+ body_modified = true
15
+ request_body.delete('public_key')
16
+ else
17
+ updating_public_key = true
18
+ end
19
+
20
+ # Generate private_key if requested.
21
+ if request_body.has_key?('private_key')
22
+ body_modified = true
23
+ if request_body['private_key']
24
+ private_key, public_key = server.gen_key_pair
25
+ updating_public_key = true
26
+ request_body['public_key'] = public_key
27
+ end
28
+ request_body.delete('private_key')
29
+ end
30
+
31
+ # Save request
32
+ request.body = JSON.pretty_generate(request_body) if body_modified
33
+
34
+ # PUT /clients is patchy
35
+ request.body = patch_request_body(request)
36
+
37
+ result = super(request)
38
+
39
+ # Inject private_key into response, delete public_key/password if applicable
40
+ if result[0] == 200
41
+ response = JSON.parse(result[2], :create_additions => false)
42
+ response['private_key'] = private_key if private_key
43
+ response.delete('public_key') if !updating_public_key && request.rest_path[0] == 'users'
44
+ response.delete('password')
45
+ # For PUT /clients, a rename returns 201.
46
+ if request_body['name'] && request.rest_path[1] != request_body['name']
47
+ json_response(201, response)
48
+ else
49
+ json_response(200, response)
50
+ end
51
+ else
52
+ result
53
+ end
54
+ end
55
+
56
+ def populate_defaults(request, response_json)
57
+ response = JSON.parse(response_json, :create_additions => false)
58
+ if request.rest_path[0] == 'clients'
59
+ response = DataNormalizer.normalize_client(response, request.rest_path[1])
60
+ else
61
+ response = DataNormalizer.normalize_user(response, request.rest_path[1])
62
+ end
63
+ JSON.pretty_generate(response)
64
+ end
65
+ end
66
+ end
67
+ end
68
+
@@ -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,21 @@
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
+ user = data['users'][name]
13
+ verified = user && JSON.parse(user, :create_additions => false)['password'] == password
14
+ json_response(200, {
15
+ 'name' => name,
16
+ 'verified' => !!verified
17
+ })
18
+ end
19
+ end
20
+ end
21
+ 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(data['cookbooks'], {}, 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(data['cookbooks'], {}, 1) do |name, versions|
21
+ if versions.size > 0
22
+ cookbook = JSON.parse(data['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 => get_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| Chef::Version.new(version) }
35
+ sorted[-1]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,106 @@
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(get_data(request, request.rest_path[0..1]).keys)
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
+ data['cookbooks'][name] = {} if !data['cookbooks'][name]
21
+ existing_cookbook = data['cookbooks'][name][version]
22
+
23
+ # Honor frozen
24
+ if existing_cookbook
25
+ existing_cookbook_json = JSON.parse(existing_cookbook, :create_additions => false)
26
+ if existing_cookbook_json['frozen?']
27
+ if request.query_params['force'] != "true"
28
+ raise RestErrorResponse.new(409, "The cookbook #{name} at version #{version} is frozen. Use the 'force' option to override.")
29
+ end
30
+ # For some reason, you are forever unable to modify "frozen?" on a frozen cookbook.
31
+ request_body = JSON.parse(request.body, :create_additions => false)
32
+ if !request_body['frozen?']
33
+ request_body['frozen?'] = true
34
+ request.body = JSON.pretty_generate(request_body)
35
+ end
36
+ end
37
+ end
38
+
39
+ # Set the cookbook
40
+ data['cookbooks'][name][version] = request.body
41
+
42
+ # If the cookbook was updated, check for deleted files and clean them up
43
+ if existing_cookbook
44
+ missing_checksums = get_checksums(existing_cookbook) - get_checksums(request.body)
45
+ if missing_checksums.size > 0
46
+ hoover_unused_checksums(missing_checksums)
47
+ end
48
+ end
49
+
50
+ already_json_response(existing_cookbook ? 200 : 201, populate_defaults(request, data['cookbooks'][name][version]))
51
+ end
52
+
53
+ def delete(request)
54
+ if request.rest_path[2] == "_latest" || request.rest_path[2] == "latest"
55
+ request.rest_path[2] = latest_version(get_data(request, request.rest_path[0..1]).keys)
56
+ end
57
+
58
+ deleted_cookbook = get_data(request, request.rest_path)
59
+ response = super(request)
60
+ cookbook_name = request.rest_path[1]
61
+ data['cookbooks'].delete(cookbook_name) if data['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
80
+ end
81
+
82
+ def hoover_unused_checksums(deleted_checksums)
83
+ data['cookbooks'].each_pair do |cookbook_name, versions|
84
+ versions.each_pair do |cookbook_version, cookbook|
85
+ deleted_checksums = deleted_checksums - get_checksums(cookbook)
86
+ end
87
+ end
88
+ deleted_checksums.each do |checksum|
89
+ data['file_store'].delete(checksum)
90
+ end
91
+ end
92
+
93
+ def populate_defaults(request, response_json)
94
+ # Inject URIs into each cookbook file
95
+ cookbook = JSON.parse(response_json, :create_additions => false)
96
+ cookbook = DataNormalizer.normalize_cookbook(cookbook, request.rest_path[1], request.rest_path[2], request.base_uri, request.method)
97
+ JSON.pretty_generate(cookbook)
98
+ end
99
+
100
+ def latest_version(versions)
101
+ sorted = versions.sort_by { |version| Chef::Version.new(version) }
102
+ sorted[-1]
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,59 @@
1
+ require 'json'
2
+ require 'chef/exceptions' # Needed so Chef::Version/VersionConstraint load
3
+ require 'chef/version_class'
4
+ require 'chef/version_constraint'
5
+ require 'chef_zero/rest_base'
6
+ require 'chef_zero/data_normalizer'
7
+
8
+ module ChefZero
9
+ module Endpoints
10
+ # Common code for endpoints that return cookbook lists
11
+ class CookbooksBase < RestBase
12
+ def format_cookbooks_list(request, cookbooks_list, constraints = {}, num_versions = nil)
13
+ results = {}
14
+ filter_cookbooks(cookbooks_list, constraints, num_versions) do |name, versions|
15
+ versions_list = versions.map do |version|
16
+ {
17
+ 'url' => build_uri(request.base_uri, ['cookbooks', name, version]),
18
+ 'version' => version
19
+ }
20
+ end
21
+ results[name] = {
22
+ 'url' => build_uri(request.base_uri, ['cookbooks', name]),
23
+ 'versions' => versions_list
24
+ }
25
+ end
26
+ results
27
+ end
28
+
29
+ def filter_cookbooks(cookbooks_list, constraints = {}, num_versions = nil)
30
+ cookbooks_list.keys.sort.each do |name|
31
+ constraint = Chef::VersionConstraint.new(constraints[name])
32
+ versions = []
33
+ cookbooks_list[name].keys.sort_by { |version| Chef::Version.new(version) }.reverse.each do |version|
34
+ break if num_versions && versions.size >= num_versions
35
+ if constraint.include?(version)
36
+ versions << version
37
+ end
38
+ end
39
+ yield [name, versions]
40
+ end
41
+ end
42
+
43
+ def recipe_names(cookbook_name, cookbook)
44
+ result = []
45
+ if cookbook['recipes']
46
+ cookbook['recipes'].each do |recipe|
47
+ if recipe['path'] == "recipes/#{recipe['name']}" && recipe['name'][-3..-1] == '.rb'
48
+ if recipe['name'] == 'default.rb'
49
+ result << cookbook_name
50
+ end
51
+ result << "#{cookbook_name}::#{recipe['name'][0..-4]}"
52
+ end
53
+ end
54
+ end
55
+ result
56
+ end
57
+ end
58
+ end
59
+ end