chef-zero 0.9

Sign up to get free protection for your applications and to get access to all the features.
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