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,30 @@
1
+ require 'json'
2
+ require 'chef_zero'
3
+ require 'chef_zero/rest_base'
4
+
5
+ module ChefZero
6
+ module Endpoints
7
+ # /principals/NAME
8
+ class PrincipalEndpoint < RestBase
9
+ def get(request)
10
+ name = request.rest_path[-1]
11
+ json = data['users'][name]
12
+ if json
13
+ type = 'user'
14
+ else
15
+ json = data['clients'][name]
16
+ type = 'client'
17
+ end
18
+ if json
19
+ json_response(200, {
20
+ 'name' => name,
21
+ 'type' => type,
22
+ 'public_key' => JSON.parse(json)['public_key'] || PUBLIC_KEY
23
+ })
24
+ else
25
+ error(404, 'Principal not found')
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,41 @@
1
+ require 'json'
2
+ require 'chef_zero/rest_base'
3
+
4
+ module ChefZero
5
+ module Endpoints
6
+ # Typical REST list endpoint (/roles or /data/BAG)
7
+ class RestListEndpoint < RestBase
8
+ def initialize(server, identity_key = 'name')
9
+ super(server)
10
+ @identity_key = identity_key
11
+ end
12
+
13
+ attr_reader :identity_key
14
+
15
+ def get(request)
16
+ # Get the result
17
+ result_hash = {}
18
+ get_data(request).keys.sort.each do |name|
19
+ result_hash[name] = "#{build_uri(request.base_uri, request.rest_path + [name])}"
20
+ end
21
+ json_response(200, result_hash)
22
+ end
23
+
24
+ def post(request)
25
+ container = get_data(request)
26
+ contents = request.body
27
+ key = get_key(contents)
28
+ if container[key]
29
+ error(409, 'Object already exists')
30
+ else
31
+ container[key] = contents
32
+ json_response(201, {'uri' => "#{build_uri(request.base_uri, request.rest_path + [key])}"})
33
+ end
34
+ end
35
+
36
+ def get_key(contents)
37
+ JSON.parse(contents, :create_additions => false)[identity_key]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,65 @@
1
+ require 'json'
2
+ require 'chef_zero/rest_base'
3
+ require 'chef_zero/rest_error_response'
4
+
5
+ module ChefZero
6
+ module Endpoints
7
+ # Typical REST leaf endpoint (/roles/NAME or /data/BAG/NAME)
8
+ class RestObjectEndpoint < RestBase
9
+ def initialize(server, identity_key = 'name')
10
+ super(server)
11
+ @identity_key = identity_key
12
+ end
13
+
14
+ attr_reader :identity_key
15
+
16
+ def get(request)
17
+ already_json_response(200, populate_defaults(request, get_data(request)))
18
+ end
19
+
20
+ def put(request)
21
+ # We grab the old body to trigger a 404 if it doesn't exist
22
+ old_body = get_data(request)
23
+ request_json = JSON.parse(request.body, :create_additions => false)
24
+ key = request_json[identity_key] || request.rest_path[-1]
25
+ container = get_data(request, request.rest_path[0..-2])
26
+ # If it's a rename, check for conflict and delete the old value
27
+ rename = key != request.rest_path[-1]
28
+ if rename
29
+ if container.has_key?(key)
30
+ return error(409, "Cannot rename '#{request.rest_path[-1]}' to '#{key}': '#{key}' already exists")
31
+ end
32
+ container.delete(request.rest_path[-1])
33
+ end
34
+ container[key] = request.body
35
+ already_json_response(200, populate_defaults(request, request.body))
36
+ end
37
+
38
+ def delete(request)
39
+ key = request.rest_path[-1]
40
+ container = get_data(request, request.rest_path[0..-2])
41
+ if !container.has_key?(key)
42
+ raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}")
43
+ end
44
+ result = container[key]
45
+ container.delete(key)
46
+ already_json_response(200, populate_defaults(request, result))
47
+ end
48
+
49
+ def patch_request_body(request)
50
+ container = get_data(request, request.rest_path[0..-2])
51
+ existing_value = container[request.rest_path[-1]]
52
+ if existing_value
53
+ request_json = JSON.parse(request.body, :create_additions => false)
54
+ existing_json = JSON.parse(existing_value, :create_additions => false)
55
+ merged_json = existing_json.merge(request_json)
56
+ if merged_json.size > request_json.size
57
+ return JSON.pretty_generate(merged_json)
58
+ end
59
+ end
60
+ request.body
61
+ end
62
+ end
63
+ end
64
+ end
65
+
@@ -0,0 +1,16 @@
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
+ # /roles/NAME
8
+ class RoleEndpoint < RestObjectEndpoint
9
+ def populate_defaults(request, response_json)
10
+ role = JSON.parse(response_json, :create_additions => false)
11
+ role = DataNormalizer.normalize_role(role, request.rest_path[1])
12
+ JSON.pretty_generate(role)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ require 'json'
2
+ require 'chef_zero/rest_base'
3
+
4
+ module ChefZero
5
+ module Endpoints
6
+ # /roles/NAME/environments
7
+ class RoleEnvironmentsEndpoint < RestBase
8
+ def get(request)
9
+ role = JSON.parse(get_data(request, request.rest_path[0..1]), :create_additions => false)
10
+ json_response(200, [ '_default' ] + (role['env_run_lists'].keys || []))
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ require 'chef_zero/rest_base'
2
+
3
+ module ChefZero
4
+ module Endpoints
5
+ # /sandboxes/ID
6
+ class SandboxEndpoint < RestBase
7
+ def put(request)
8
+ existing_sandbox = get_data(request, request.rest_path)
9
+ data['sandboxes'].delete(request.rest_path[1])
10
+ time_str = existing_sandbox[:create_time].strftime('%Y-%m-%dT%H:%M:%S%z')
11
+ time_str = "#{time_str[0..21]}:#{time_str[22..23]}"
12
+ json_response(200, {
13
+ :guid => request.rest_path[1],
14
+ :name => request.rest_path[1],
15
+ :checksums => existing_sandbox[:checksums],
16
+ :create_time => time_str,
17
+ :is_completed => true
18
+ })
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ require 'json'
2
+ require 'chef_zero/rest_base'
3
+
4
+ module ChefZero
5
+ module Endpoints
6
+ # /sandboxes
7
+ class SandboxesEndpoint < RestBase
8
+ def initialize(server)
9
+ super(server)
10
+ @next_id = 1
11
+ end
12
+
13
+ def post(request)
14
+ sandbox_checksums = []
15
+
16
+ needed_checksums = JSON.parse(request.body, :create_additions => false)['checksums']
17
+ result_checksums = {}
18
+ needed_checksums.keys.each do |needed_checksum|
19
+ if data['file_store'].has_key?(needed_checksum)
20
+ result_checksums[needed_checksum] = { :needs_upload => false }
21
+ else
22
+ result_checksums[needed_checksum] = {
23
+ :needs_upload => true,
24
+ :url => build_uri(request.base_uri, ['file_store', needed_checksum])
25
+ }
26
+ sandbox_checksums << needed_checksum
27
+ end
28
+ end
29
+
30
+ id = @next_id.to_s
31
+ @next_id+=1
32
+
33
+ data['sandboxes'][id] = { :create_time => Time.now.utc, :checksums => sandbox_checksums }
34
+
35
+ json_response(201, {
36
+ :uri => build_uri(request.base_uri, request.rest_path + [id.to_s]),
37
+ :checksums => result_checksums,
38
+ :sandbox_id => id
39
+ })
40
+ end
41
+ end
42
+ end
43
+ end
44
+
@@ -0,0 +1,139 @@
1
+ require 'json'
2
+ require 'chef/mixin/deep_merge'
3
+ require 'chef_zero/endpoints/rest_object_endpoint'
4
+ require 'chef_zero/data_normalizer'
5
+ require 'chef_zero/rest_error_response'
6
+ require 'chef_zero/solr/solr_parser'
7
+ require 'chef_zero/solr/solr_doc'
8
+
9
+ module ChefZero
10
+ module Endpoints
11
+ # /search/INDEX
12
+ class SearchEndpoint < RestBase
13
+ def get(request)
14
+ results = search(request)
15
+ results['rows'] = results['rows'].map { |name,uri,value,search_value| value }
16
+ json_response(200, results)
17
+ end
18
+
19
+ def post(request)
20
+ full_results = search(request)
21
+ keys = JSON.parse(request.body, :create_additions => false)
22
+ partial_results = full_results['rows'].map do |name, uri, doc, search_value|
23
+ data = {}
24
+ keys.each_pair do |key, path|
25
+ if path.size > 0
26
+ value = search_value
27
+ path.each do |path_part|
28
+ value = value[path_part] if !value.nil?
29
+ end
30
+ data[key] = value
31
+ else
32
+ data[key] = nil
33
+ end
34
+ end
35
+ {
36
+ 'url' => uri,
37
+ 'data' => data
38
+ }
39
+ end
40
+ json_response(200, {
41
+ 'rows' => partial_results,
42
+ 'start' => full_results['start'],
43
+ 'total' => full_results['total']
44
+ })
45
+ end
46
+
47
+ private
48
+
49
+ def search_container(request, index)
50
+ case index
51
+ when 'client'
52
+ [ data['clients'], Proc.new { |client, name| DataNormalizer.normalize_client(client, name) }, build_uri(request.base_uri, [ 'clients' ]) ]
53
+ when 'node'
54
+ [ data['nodes'], Proc.new { |node, name| DataNormalizer.normalize_node(node, name) }, build_uri(request.base_uri, [ 'nodes' ]) ]
55
+ when 'environment'
56
+ [ data['environments'], Proc.new { |environment, name| DataNormalizer.normalize_environment(environment, name) }, build_uri(request.base_uri, [ 'environments' ]) ]
57
+ when 'role'
58
+ [ data['roles'], Proc.new { |role, name| DataNormalizer.normalize_role(role, name) }, build_uri(request.base_uri, [ 'roles' ]) ]
59
+ else
60
+ [ data['data'][index], Proc.new { |data_bag_item, id| DataNormalizer.normalize_data_bag_item(data_bag_item, index, id, 'DELETE') }, build_uri(request.base_uri, [ 'data', index ]) ]
61
+ end
62
+ end
63
+
64
+ def expand_for_indexing(value, index, id)
65
+ if index == 'node'
66
+ result = {}
67
+ Chef::Mixin::DeepMerge.deep_merge!(value['default'] || {}, result)
68
+ Chef::Mixin::DeepMerge.deep_merge!(value['normal'] || {}, result)
69
+ Chef::Mixin::DeepMerge.deep_merge!(value['override'] || {}, result)
70
+ Chef::Mixin::DeepMerge.deep_merge!(value['automatic'] || {}, result)
71
+ result['recipe'] = []
72
+ result['role'] = []
73
+ if value['run_list']
74
+ value['run_list'].each do |run_list_entry|
75
+ if run_list_entry =~ /^(recipe|role)\[(.*)\]/
76
+ result[$1] << $2
77
+ end
78
+ end
79
+ end
80
+ value.each_pair do |key, value|
81
+ result[key] = value unless %w(default normal override automatic).include?(key)
82
+ end
83
+ result
84
+
85
+ elsif !%w(client environment role).include?(index)
86
+ DataNormalizer.normalize_data_bag_item(value, index, id, 'GET')
87
+ else
88
+ value
89
+ end
90
+ end
91
+
92
+ def search(request)
93
+ # Extract parameters
94
+ index = request.rest_path[1]
95
+ query_string = request.query_params['q'] || '*:*'
96
+ solr_query = ChefZero::Solr::SolrParser.new(query_string).parse
97
+ sort_string = request.query_params['sort']
98
+ start = request.query_params['start']
99
+ start = start.to_i if start
100
+ rows = request.query_params['rows']
101
+ rows = rows.to_i if rows
102
+
103
+ # Get the search container
104
+ container, expander, base_uri = search_container(request, index)
105
+ if container.nil?
106
+ raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}")
107
+ end
108
+
109
+ # Search!
110
+ result = []
111
+ container.each_pair do |name,value|
112
+ expanded = expander.call(JSON.parse(value, :create_additions => false), name)
113
+ result << [ name, build_uri(base_uri, [name]), expanded, expand_for_indexing(expanded, index, name) ]
114
+ end
115
+ result = result.select do |name, uri, value, search_value|
116
+ solr_query.matches_doc?(ChefZero::Solr::SolrDoc.new(search_value, name))
117
+ end
118
+ total = result.size
119
+
120
+ # Sort
121
+ if sort_string
122
+ sort_key, sort_order = sort_string.split(/\s+/, 2)
123
+ result = result.sort_by { |name,uri,value,search_value| ChefZero::Solr::SolrDoc.new(search_value, name)[sort_key] }
124
+ result = result.reverse if sort_order == "DESC"
125
+ end
126
+
127
+ # Paginate
128
+ if start
129
+ result = result[start..start+(rows||-1)]
130
+ end
131
+ {
132
+ 'rows' => result,
133
+ 'start' => start || 0,
134
+ 'total' => total
135
+ }
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,18 @@
1
+ require 'chef_zero/rest_base'
2
+
3
+ module ChefZero
4
+ module Endpoints
5
+ # /search
6
+ class SearchesEndpoint < RestBase
7
+ def get(request)
8
+ # Get the result
9
+ result_hash = {}
10
+ indices = (%w(client environment node role) + data['data'].keys).sort
11
+ indices.each do |index|
12
+ result_hash[index] = build_uri(request.base_uri, request.rest_path + [index])
13
+ end
14
+ json_response(200, result_hash)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,82 @@
1
+ require 'chef_zero/rest_request'
2
+ require 'chef_zero/rest_error_response'
3
+ require 'chef/log'
4
+
5
+ module ChefZero
6
+ class RestBase
7
+ def initialize(server)
8
+ @server = server
9
+ end
10
+
11
+ attr_reader :server
12
+
13
+ def data
14
+ server.data
15
+ end
16
+
17
+ def call(env)
18
+ begin
19
+ rest_path = env['PATH_INFO'].split('/').select { |part| part != "" }
20
+ method = env['REQUEST_METHOD'].downcase.to_sym
21
+ if !self.respond_to?(method)
22
+ accept_methods = [:get, :put, :post, :delete].select { |m| self.respond_to?(m) }
23
+ accept_methods_str = accept_methods.map { |m| m.to_s.upcase }.join(', ')
24
+ return [405, {"Content-Type" => "text/plain", "Allow" => accept_methods_str}, "Bad request method for '#{env['REQUEST_PATH']}': #{env['REQUEST_METHOD']}"]
25
+ end
26
+ if json_only && !env['HTTP_ACCEPT'].split(';').include?('application/json')
27
+ return [406, {"Content-Type" => "text/plain"}, "Must accept application/json"]
28
+ end
29
+ # Dispatch to get()/post()/put()/delete()
30
+ begin
31
+ self.send(method, RestRequest.new(env))
32
+ rescue RestErrorResponse => e
33
+ error(e.response_code, e.error)
34
+ end
35
+ rescue
36
+ Chef::Log.error("#{$!.inspect}\n#{$!.backtrace.join("\n")}")
37
+ raise
38
+ end
39
+ end
40
+
41
+ def json_only
42
+ true
43
+ end
44
+
45
+ def get_data(request, rest_path=nil)
46
+ rest_path ||= request.rest_path
47
+ # Grab the value we're looking for
48
+ value = data
49
+ rest_path.each do |path_part|
50
+ if !value.has_key?(path_part)
51
+ raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
52
+ end
53
+ value = value[path_part]
54
+ end
55
+ value
56
+ end
57
+
58
+ def error(response_code, error)
59
+ json_response(response_code, {"error" => [error]})
60
+ end
61
+
62
+ def json_response(response_code, json)
63
+ already_json_response(response_code, JSON.pretty_generate(json))
64
+ end
65
+
66
+ def already_json_response(response_code, json_text)
67
+ [response_code, {"Content-Type" => "application/json"}, json_text]
68
+ end
69
+
70
+ def build_uri(base_uri, rest_path)
71
+ RestBase::build_uri(base_uri, rest_path)
72
+ end
73
+
74
+ def self.build_uri(base_uri, rest_path)
75
+ "#{base_uri}/#{rest_path.join('/')}"
76
+ end
77
+
78
+ def populate_defaults(request, response)
79
+ response
80
+ end
81
+ end
82
+ end