1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
require "ffi_yajl"
require "chef_zero/endpoints/rest_object_endpoint"
require "chef_zero/rest_error_response"
require "chef_zero/chef_data/data_normalizer"
require "chef_zero/data_store/data_not_found_error"
module ChefZero
module Endpoints
# /organizations/ORG/cookbooks/NAME/VERSION
class CookbookVersionEndpoint < RestObjectEndpoint
def get(request)
if request.rest_path[4] == "_latest" || request.rest_path[4] == "latest"
request.rest_path[4] = latest_version(list_data(request, request.rest_path[0..3]))
end
super(request)
end
def put(request)
name = request.rest_path[3]
version = request.rest_path[4]
existing_cookbook = get_data(request, request.rest_path, :nil)
# Honor frozen
if existing_cookbook
existing_cookbook_json = FFI_Yajl::Parser.parse(existing_cookbook)
if existing_cookbook_json["frozen?"]
if request.query_params["force"] != "true"
raise RestErrorResponse.new(409, "The cookbook #{name} at version #{version} is frozen. Use the 'force' option to override.")
end
# For some reason, you are forever unable to modify "frozen?" on a frozen cookbook.
request_body = FFI_Yajl::Parser.parse(request.body)
if !request_body["frozen?"]
request_body["frozen?"] = true
request.body = FFI_Yajl::Encoder.encode(request_body, :pretty => true)
end
end
end
# Set the cookbook
set_data(request, request.rest_path, request.body, :create_dir, :create)
# If the cookbook was updated, check for deleted files and clean them up
if existing_cookbook
missing_checksums = get_checksums(existing_cookbook) - get_checksums(request.body)
if missing_checksums.size > 0
hoover_unused_checksums(missing_checksums, request)
end
end
already_json_response(existing_cookbook ? 200 : 201, populate_defaults(request, request.body))
end
def delete(request)
if request.rest_path[4] == "_latest" || request.rest_path[4] == "latest"
request.rest_path[4] = latest_version(list_data(request, request.rest_path[0..3]))
end
deleted_cookbook = get_data(request)
response = super(request)
# Last one out turns out the lights: delete /organizations/ORG/cookbooks/NAME if it no longer has versions
cookbook_path = request.rest_path[0..3]
if exists_data_dir?(request, cookbook_path) && list_data(request, cookbook_path).size == 0
delete_data_dir(request, cookbook_path)
end
# Hoover deleted files, if they exist
hoover_unused_checksums(get_checksums(deleted_cookbook), request)
response
end
def get_checksums(cookbook)
result = []
FFI_Yajl::Parser.parse(cookbook).each_pair do |key, value|
if value.is_a?(Array)
value.each do |file|
if file.is_a?(Hash) && file.has_key?("checksum")
result << file["checksum"]
end
end
end
end
result.uniq
end
private
def hoover_unused_checksums(deleted_checksums, request)
%w{cookbooks cookbook_artifacts}.each do |cookbook_type|
begin
cookbooks = data_store.list(request.rest_path[0..1] + [cookbook_type])
rescue ChefZero::DataStore::DataNotFoundError
# Not all chef versions support cookbook_artifacts
raise unless cookbook_type == "cookbook_artifacts"
cookbooks = []
end
cookbooks.each do |cookbook_name|
# as below, this can be racy.
begin
data_store.list(request.rest_path[0..1] + [cookbook_type, cookbook_name]).each do |version|
cookbook = data_store.get(request.rest_path[0..1] + [cookbook_type, cookbook_name, version], request)
deleted_checksums = deleted_checksums - get_checksums(cookbook)
end
rescue ChefZero::DataStore::DataNotFoundError
end
end
end
deleted_checksums.each do |checksum|
# There can be a race here if multiple cookbooks are uploading.
# This deals with an exception on delete, but things can still get deleted
# that shouldn't be.
begin
delete_data(request, request.rest_path[0..1] + ["file_store", "checksums", checksum], :data_store_exceptions)
rescue ChefZero::DataStore::DataNotFoundError
end
end
end
def populate_defaults(request, response_json)
# Inject URIs into each cookbook file
cookbook = FFI_Yajl::Parser.parse(response_json)
cookbook = ChefData::DataNormalizer.normalize_cookbook(self, request.rest_path[0..1], cookbook, request.rest_path[3], request.rest_path[4], request.base_uri, request.method)
FFI_Yajl::Encoder.encode(cookbook, :pretty => true)
end
def latest_version(versions)
sorted = versions.sort_by { |version| Gem::Version.new(version.dup) }
sorted[-1]
end
end
end
end
|