chef-infra-api 0.9.1
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.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/lib/chef-api.rb +96 -0
- data/lib/chef-api/aclable.rb +35 -0
- data/lib/chef-api/authentication.rb +300 -0
- data/lib/chef-api/boolean.rb +6 -0
- data/lib/chef-api/configurable.rb +80 -0
- data/lib/chef-api/connection.rb +507 -0
- data/lib/chef-api/defaults.rb +197 -0
- data/lib/chef-api/error_collection.rb +44 -0
- data/lib/chef-api/errors.rb +64 -0
- data/lib/chef-api/multipart.rb +164 -0
- data/lib/chef-api/resource.rb +21 -0
- data/lib/chef-api/resources/base.rb +960 -0
- data/lib/chef-api/resources/client.rb +84 -0
- data/lib/chef-api/resources/collection_proxy.rb +234 -0
- data/lib/chef-api/resources/cookbook.rb +24 -0
- data/lib/chef-api/resources/cookbook_version.rb +23 -0
- data/lib/chef-api/resources/data_bag.rb +136 -0
- data/lib/chef-api/resources/data_bag_item.rb +53 -0
- data/lib/chef-api/resources/environment.rb +16 -0
- data/lib/chef-api/resources/group.rb +16 -0
- data/lib/chef-api/resources/node.rb +20 -0
- data/lib/chef-api/resources/organization.rb +22 -0
- data/lib/chef-api/resources/partial_search.rb +44 -0
- data/lib/chef-api/resources/principal.rb +11 -0
- data/lib/chef-api/resources/role.rb +18 -0
- data/lib/chef-api/resources/search.rb +47 -0
- data/lib/chef-api/resources/user.rb +82 -0
- data/lib/chef-api/schema.rb +150 -0
- data/lib/chef-api/util.rb +119 -0
- data/lib/chef-api/validator.rb +16 -0
- data/lib/chef-api/validators/base.rb +82 -0
- data/lib/chef-api/validators/required.rb +11 -0
- data/lib/chef-api/validators/type.rb +23 -0
- data/lib/chef-api/version.rb +3 -0
- data/templates/errors/abstract_method.erb +5 -0
- data/templates/errors/cannot_regenerate_key.erb +1 -0
- data/templates/errors/chef_api_error.erb +1 -0
- data/templates/errors/file_not_found.erb +1 -0
- data/templates/errors/http_bad_request.erb +3 -0
- data/templates/errors/http_forbidden_request.erb +3 -0
- data/templates/errors/http_gateway_timeout.erb +3 -0
- data/templates/errors/http_method_not_allowed.erb +3 -0
- data/templates/errors/http_not_acceptable.erb +3 -0
- data/templates/errors/http_not_found.erb +3 -0
- data/templates/errors/http_server_unavailable.erb +1 -0
- data/templates/errors/http_unauthorized_request.erb +3 -0
- data/templates/errors/insufficient_file_permissions.erb +1 -0
- data/templates/errors/invalid_resource.erb +1 -0
- data/templates/errors/invalid_validator.erb +1 -0
- data/templates/errors/missing_url_parameter.erb +1 -0
- data/templates/errors/not_a_directory.erb +1 -0
- data/templates/errors/resource_already_exists.erb +1 -0
- data/templates/errors/resource_not_found.erb +1 -0
- data/templates/errors/resource_not_mutable.erb +1 -0
- data/templates/errors/unknown_attribute.erb +1 -0
- metadata +130 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
require "chef-api/aclable"
|
2
|
+
module ChefAPI
|
3
|
+
module Resource
|
4
|
+
autoload :Base, 'chef-api/resources/base'
|
5
|
+
autoload :Client, 'chef-api/resources/client'
|
6
|
+
autoload :CollectionProxy, 'chef-api/resources/collection_proxy'
|
7
|
+
autoload :Cookbook, 'chef-api/resources/cookbook'
|
8
|
+
autoload :CookbookVersion, 'chef-api/resources/cookbook_version'
|
9
|
+
autoload :DataBag, 'chef-api/resources/data_bag'
|
10
|
+
autoload :DataBagItem, 'chef-api/resources/data_bag_item'
|
11
|
+
autoload :Environment, 'chef-api/resources/environment'
|
12
|
+
autoload :Group, 'chef-api/resources/group'
|
13
|
+
autoload :Node, 'chef-api/resources/node'
|
14
|
+
autoload :Organization, 'chef-api/resources/organization'
|
15
|
+
autoload :PartialSearch, 'chef-api/resources/partial_search'
|
16
|
+
autoload :Principal, 'chef-api/resources/principal'
|
17
|
+
autoload :Role, 'chef-api/resources/role'
|
18
|
+
autoload :Search, 'chef-api/resources/search'
|
19
|
+
autoload :User, 'chef-api/resources/user'
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,960 @@
|
|
1
|
+
module ChefAPI
|
2
|
+
class Resource::Base
|
3
|
+
class << self
|
4
|
+
# Including the Enumberable module gives us magic
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
#
|
8
|
+
# Load the given resource from it's on-disk equivalent. This action only
|
9
|
+
# makes sense for some resources, and must be defined on a per-resource
|
10
|
+
# basis, since the logic varies between resources.
|
11
|
+
#
|
12
|
+
# @param [String] path
|
13
|
+
# the path to the file on disk
|
14
|
+
#
|
15
|
+
def from_file(path)
|
16
|
+
raise Error::AbstractMethod.new(method: 'Resource::Base#from_file')
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# @todo doc
|
21
|
+
#
|
22
|
+
def from_url(url, prefix = {})
|
23
|
+
from_json(connection.get(url), prefix)
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Get or set the schema for the remote resource. You probably only want
|
28
|
+
# to call schema once with a block, because it will overwrite the
|
29
|
+
# existing schema (meaning entries are not merged). If a block is given,
|
30
|
+
# a new schema object is created, otherwise the current one is returned.
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# schema do
|
34
|
+
# attribute :id, primary: true
|
35
|
+
# attribute :name, type: String, default: 'bacon'
|
36
|
+
# attribute :admin, type: Boolean, required: true
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# @return [Schema]
|
40
|
+
# the schema object for this resource
|
41
|
+
#
|
42
|
+
def schema(&block)
|
43
|
+
if block
|
44
|
+
@schema = Schema.new(&block)
|
45
|
+
else
|
46
|
+
@schema
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Protect one or more resources from being altered by the user. This is
|
52
|
+
# useful if there's an admin client or magical cookbook that you don't
|
53
|
+
# want users to modify.
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# protect 'chef-webui', 'my-magical-validator'
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# protect ->(resource) { resource.name =~ /internal_(.+)/ }
|
60
|
+
#
|
61
|
+
# @param [Array<String, Proc>] ids
|
62
|
+
# the list of "things" to protect
|
63
|
+
#
|
64
|
+
def protect(*ids)
|
65
|
+
ids.each { |id| protected_resources << id }
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Create a nested relationship collection. The associated collection
|
70
|
+
# is cached on the class, reducing API requests.
|
71
|
+
#
|
72
|
+
# @example Create an association to environments
|
73
|
+
#
|
74
|
+
# has_many :environments
|
75
|
+
#
|
76
|
+
# @example Create an association with custom configuration
|
77
|
+
#
|
78
|
+
# has_many :environments, class_name: 'Environment'
|
79
|
+
#
|
80
|
+
def has_many(method, options = {})
|
81
|
+
class_name = options[:class_name] || "Resource::#{Util.camelize(method).sub(/s$/, '')}"
|
82
|
+
rest_endpoint = options[:rest_endpoint] || method
|
83
|
+
|
84
|
+
class_eval <<-EOH, __FILE__, __LINE__ + 1
|
85
|
+
def #{method}
|
86
|
+
associations[:#{method}] ||=
|
87
|
+
Resource::CollectionProxy.new(self, #{class_name}, '#{rest_endpoint}')
|
88
|
+
end
|
89
|
+
EOH
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# @todo doc
|
94
|
+
#
|
95
|
+
def protected_resources
|
96
|
+
@protected_resources ||= []
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Get or set the name of the remote resource collection. This is most
|
101
|
+
# likely the remote API endpoint (such as +/clients+), without the
|
102
|
+
# leading slash.
|
103
|
+
#
|
104
|
+
# @example Setting a base collection path
|
105
|
+
# collection_path '/clients'
|
106
|
+
#
|
107
|
+
# @example Setting a collection path with nesting
|
108
|
+
# collection_path '/data/:name'
|
109
|
+
#
|
110
|
+
# @param [Symbol] value
|
111
|
+
# the value to use for the collection name.
|
112
|
+
#
|
113
|
+
# @return [Symbol, String]
|
114
|
+
# the name of the collection
|
115
|
+
#
|
116
|
+
def collection_path(value = UNSET)
|
117
|
+
if value != UNSET
|
118
|
+
@collection_path = value.to_s
|
119
|
+
else
|
120
|
+
@collection_path ||
|
121
|
+
raise(ArgumentError, "collection_path not set for #{self.class}")
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
#
|
126
|
+
# Make an authenticated HTTP POST request using the connection object.
|
127
|
+
# This method returns a new object representing the response from the
|
128
|
+
# server, which should be merged with an existing object's attributes to
|
129
|
+
# reflect the newest state of the resource.
|
130
|
+
#
|
131
|
+
# @param [Hash] body
|
132
|
+
# the request body to create the resource with (probably JSON)
|
133
|
+
# @param [Hash] prefix
|
134
|
+
# the list of prefix options (for nested resources)
|
135
|
+
#
|
136
|
+
# @return [String]
|
137
|
+
# the JSON response from the server
|
138
|
+
#
|
139
|
+
def post(body, prefix = {})
|
140
|
+
path = expanded_collection_path(prefix)
|
141
|
+
connection.post(path, body)
|
142
|
+
end
|
143
|
+
|
144
|
+
#
|
145
|
+
# Perform a PUT request to the Chef Server against the given resource or
|
146
|
+
# resource identifier. The resource will be partially updated (this
|
147
|
+
# method doubles as PATCH) with the given parameters.
|
148
|
+
#
|
149
|
+
# @param [String, Resource::Base] id
|
150
|
+
# a resource object or a string representing the unique identifier of
|
151
|
+
# the resource object to update
|
152
|
+
# @param [Hash] body
|
153
|
+
# the request body to create the resource with (probably JSON)
|
154
|
+
# @param [Hash] prefix
|
155
|
+
# the list of prefix options (for nested resources)
|
156
|
+
#
|
157
|
+
# @return [String]
|
158
|
+
# the JSON response from the server
|
159
|
+
#
|
160
|
+
def put(id, body, prefix = {})
|
161
|
+
path = resource_path(id, prefix)
|
162
|
+
connection.put(path, body)
|
163
|
+
end
|
164
|
+
|
165
|
+
#
|
166
|
+
# Delete the remote resource from the Chef Sserver.
|
167
|
+
#
|
168
|
+
# @param [String, Fixnum] id
|
169
|
+
# the id of the resource to delete
|
170
|
+
# @param [Hash] prefix
|
171
|
+
# the list of prefix options (for nested resources)
|
172
|
+
# @return [true]
|
173
|
+
#
|
174
|
+
def delete(id, prefix = {})
|
175
|
+
path = resource_path(id, prefix)
|
176
|
+
connection.delete(path)
|
177
|
+
true
|
178
|
+
rescue Error::HTTPNotFound
|
179
|
+
true
|
180
|
+
end
|
181
|
+
|
182
|
+
#
|
183
|
+
# Get the "list" of items in this resource. This list contains the
|
184
|
+
# primary keys of all of the resources in this collection. This method
|
185
|
+
# is useful in CLI applications, because it only makes a single API
|
186
|
+
# request to gather this data.
|
187
|
+
#
|
188
|
+
# @param [Hash] prefix
|
189
|
+
# the listof prefix options (for nested resources)
|
190
|
+
#
|
191
|
+
# @example Get the list of all clients
|
192
|
+
# Client.list #=> ['validator', 'chef-webui']
|
193
|
+
#
|
194
|
+
# @return [Array<String>]
|
195
|
+
#
|
196
|
+
def list(prefix = {})
|
197
|
+
path = expanded_collection_path(prefix)
|
198
|
+
connection.get(path).keys.sort
|
199
|
+
end
|
200
|
+
|
201
|
+
#
|
202
|
+
# Destroy a record with the given id.
|
203
|
+
#
|
204
|
+
# @param [String, Fixnum] id
|
205
|
+
# the id of the resource to delete
|
206
|
+
# @param [Hash] prefix
|
207
|
+
# the list of prefix options (for nested resources)
|
208
|
+
#
|
209
|
+
# @return [Base, nil]
|
210
|
+
# the destroyed resource, or nil if the resource does not exist on the
|
211
|
+
# remote Chef Server
|
212
|
+
#
|
213
|
+
def destroy(id, prefix = {})
|
214
|
+
resource = fetch(id, prefix)
|
215
|
+
return nil if resource.nil?
|
216
|
+
|
217
|
+
resource.destroy
|
218
|
+
resource
|
219
|
+
end
|
220
|
+
|
221
|
+
#
|
222
|
+
# Delete all remote resources of the given type from the Chef Server
|
223
|
+
#
|
224
|
+
# @param [Hash] prefix
|
225
|
+
# the list of prefix options (for nested resources)
|
226
|
+
# @return [Array<Base>]
|
227
|
+
# an array containing the list of resources that were deleted
|
228
|
+
#
|
229
|
+
def destroy_all(prefix = {})
|
230
|
+
map { |resource| resource.destroy }
|
231
|
+
end
|
232
|
+
|
233
|
+
#
|
234
|
+
# Fetch a single resource in the remote collection.
|
235
|
+
#
|
236
|
+
# @example fetch a single client
|
237
|
+
# Client.fetch('chef-webui') #=> #<Client name: 'chef-webui', ...>
|
238
|
+
#
|
239
|
+
# @param [String, Fixnum] id
|
240
|
+
# the id of the resource to fetch
|
241
|
+
# @param [Hash] prefix
|
242
|
+
# the list of prefix options (for nested resources)
|
243
|
+
#
|
244
|
+
# @return [Resource::Base, nil]
|
245
|
+
# an instance of the resource, or nil if that given resource does not
|
246
|
+
# exist
|
247
|
+
#
|
248
|
+
def fetch(id, prefix = {})
|
249
|
+
return nil if id.nil?
|
250
|
+
|
251
|
+
path = resource_path(id, prefix)
|
252
|
+
response = connection.get(path)
|
253
|
+
from_json(response, prefix)
|
254
|
+
rescue Error::HTTPNotFound
|
255
|
+
nil
|
256
|
+
end
|
257
|
+
|
258
|
+
#
|
259
|
+
# Build a new resource from the given attributes.
|
260
|
+
#
|
261
|
+
# @see ChefAPI::Resource::Base#initialize for more information
|
262
|
+
#
|
263
|
+
# @example build an empty resource
|
264
|
+
# Bacon.build #=> #<ChefAPI::Resource::Bacon>
|
265
|
+
#
|
266
|
+
# @example build a resource with attributes
|
267
|
+
# Bacon.build(foo: 'bar') #=> #<ChefAPI::Resource::Baocn foo: bar>
|
268
|
+
#
|
269
|
+
# @param [Hash] attributes
|
270
|
+
# the list of attributes for the new resource - any attributes that
|
271
|
+
# are not defined in the schema are silently ignored
|
272
|
+
#
|
273
|
+
def build(attributes = {}, prefix = {})
|
274
|
+
new(attributes, prefix)
|
275
|
+
end
|
276
|
+
|
277
|
+
#
|
278
|
+
# Create a new resource and save it to the Chef Server, raising any
|
279
|
+
# exceptions that might occur. This method will save the resource back to
|
280
|
+
# the Chef Server, raising any validation errors that occur.
|
281
|
+
#
|
282
|
+
# @raise [Error::ResourceAlreadyExists]
|
283
|
+
# if the resource with the primary key already exists on the Chef Server
|
284
|
+
# @raise [Error::InvalidResource]
|
285
|
+
# if any of the resource's validations fail
|
286
|
+
#
|
287
|
+
# @param [Hash] attributes
|
288
|
+
# the list of attributes to set on the new resource
|
289
|
+
#
|
290
|
+
# @return [Resource::Base]
|
291
|
+
# an instance of the created resource
|
292
|
+
#
|
293
|
+
def create(attributes = {}, prefix = {})
|
294
|
+
resource = build(attributes, prefix)
|
295
|
+
|
296
|
+
unless resource.new_resource?
|
297
|
+
raise Error::ResourceAlreadyExists.new
|
298
|
+
end
|
299
|
+
|
300
|
+
resource.save!
|
301
|
+
resource
|
302
|
+
end
|
303
|
+
|
304
|
+
#
|
305
|
+
# Check if the given resource exists on the Chef Server.
|
306
|
+
#
|
307
|
+
# @param [String, Fixnum] id
|
308
|
+
# the id of the resource to fetch
|
309
|
+
# @param [Hash] prefix
|
310
|
+
# the list of prefix options (for nested resources)
|
311
|
+
#
|
312
|
+
# @return [Boolean]
|
313
|
+
#
|
314
|
+
def exists?(id, prefix = {})
|
315
|
+
!fetch(id, prefix).nil?
|
316
|
+
end
|
317
|
+
|
318
|
+
#
|
319
|
+
# Perform a PUT request to the Chef Server for the current resource,
|
320
|
+
# updating the given parameters. The parameters may be a full or
|
321
|
+
# partial resource update, as supported by the Chef Server.
|
322
|
+
#
|
323
|
+
# @raise [Error::ResourceNotFound]
|
324
|
+
# if the given resource does not exist on the Chef Server
|
325
|
+
#
|
326
|
+
# @param [String, Fixnum] id
|
327
|
+
# the id of the resource to update
|
328
|
+
# @param [Hash] attributes
|
329
|
+
# the list of attributes to set on the new resource
|
330
|
+
# @param [Hash] prefix
|
331
|
+
# the list of prefix options (for nested resources)
|
332
|
+
#
|
333
|
+
# @return [Resource::Base]
|
334
|
+
# the new resource
|
335
|
+
#
|
336
|
+
def update(id, attributes = {}, prefix = {})
|
337
|
+
resource = fetch(id, prefix)
|
338
|
+
|
339
|
+
unless resource
|
340
|
+
raise Error::ResourceNotFound.new(type: type, id: id)
|
341
|
+
end
|
342
|
+
|
343
|
+
resource.update(attributes).save
|
344
|
+
resource
|
345
|
+
end
|
346
|
+
|
347
|
+
#
|
348
|
+
# (Lazy) iterate over each item in the collection, yielding the fully-
|
349
|
+
# built resource object. This method, coupled with the Enumerable
|
350
|
+
# module, provides us with other methods like +first+ and +map+.
|
351
|
+
#
|
352
|
+
# @example get the first resource
|
353
|
+
# Bacon.first #=> #<ChefAPI::Resource::Bacon>
|
354
|
+
#
|
355
|
+
# @example get the first 3 resources
|
356
|
+
# Bacon.first(3) #=> [#<ChefAPI::Resource::Bacon>, ...]
|
357
|
+
#
|
358
|
+
# @example iterate over each resource
|
359
|
+
# Bacon.each { |bacon| puts bacon.name }
|
360
|
+
#
|
361
|
+
# @example get all the resource's names
|
362
|
+
# Bacon.map(&:name) #=> ["ham", "sausage", "turkey"]
|
363
|
+
#
|
364
|
+
def each(prefix = {}, &block)
|
365
|
+
collection(prefix).each do |resource, path|
|
366
|
+
response = connection.get(path)
|
367
|
+
result = from_json(response, prefix)
|
368
|
+
|
369
|
+
block.call(result) if block
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
#
|
374
|
+
# The total number of reosurces in the collection.
|
375
|
+
#
|
376
|
+
# @return [Fixnum]
|
377
|
+
#
|
378
|
+
def count(prefix = {})
|
379
|
+
collection(prefix).size
|
380
|
+
end
|
381
|
+
alias_method :size, :count
|
382
|
+
|
383
|
+
#
|
384
|
+
# Return an array of all resources in the collection.
|
385
|
+
#
|
386
|
+
# @note Unless you need the _entire_ collection, please consider using the
|
387
|
+
# {size} and {each} methods instead as they are much more perforant.
|
388
|
+
#
|
389
|
+
# @return [Array<Resource::Base>]
|
390
|
+
#
|
391
|
+
def all
|
392
|
+
entries
|
393
|
+
end
|
394
|
+
|
395
|
+
#
|
396
|
+
# Construct the object from a JSON response. This method actually just
|
397
|
+
# delegates to the +new+ method, but it removes some marshall data and
|
398
|
+
# whatnot from the response first.
|
399
|
+
#
|
400
|
+
# @param [String] response
|
401
|
+
# the JSON response from the Chef Server
|
402
|
+
#
|
403
|
+
# @return [Resource::Base]
|
404
|
+
# an instance of the resource represented by this JSON
|
405
|
+
#
|
406
|
+
def from_json(response, prefix = {})
|
407
|
+
response.delete('json_class')
|
408
|
+
response.delete('chef_type')
|
409
|
+
|
410
|
+
new(response, prefix)
|
411
|
+
end
|
412
|
+
|
413
|
+
#
|
414
|
+
# The string representation of this class.
|
415
|
+
#
|
416
|
+
# @example for the Bacon class
|
417
|
+
# Bacon.to_s #=> "Resource::Bacon"
|
418
|
+
#
|
419
|
+
# @return [String]
|
420
|
+
#
|
421
|
+
def to_s
|
422
|
+
classname
|
423
|
+
end
|
424
|
+
|
425
|
+
#
|
426
|
+
# The detailed string representation of this class, including the full
|
427
|
+
# schema definition.
|
428
|
+
#
|
429
|
+
# @example for the Bacon class
|
430
|
+
# Bacon.inspect #=> "Resource::Bacon(id, description, ...)"
|
431
|
+
#
|
432
|
+
# @return [String]
|
433
|
+
#
|
434
|
+
def inspect
|
435
|
+
"#{classname}(#{schema.attributes.keys.join(', ')})"
|
436
|
+
end
|
437
|
+
|
438
|
+
#
|
439
|
+
# The name for this resource, minus the parent module.
|
440
|
+
#
|
441
|
+
# @example
|
442
|
+
# classname #=> Resource::Bacon
|
443
|
+
#
|
444
|
+
# @return [String]
|
445
|
+
#
|
446
|
+
def classname
|
447
|
+
name.split('::')[1..-1].join('::')
|
448
|
+
end
|
449
|
+
|
450
|
+
#
|
451
|
+
# The type of this resource.
|
452
|
+
#
|
453
|
+
# @example
|
454
|
+
# bacon
|
455
|
+
#
|
456
|
+
# @return [String]
|
457
|
+
#
|
458
|
+
def type
|
459
|
+
Util.underscore(name.split('::').last).gsub('_', ' ')
|
460
|
+
end
|
461
|
+
|
462
|
+
#
|
463
|
+
# The full collection list.
|
464
|
+
#
|
465
|
+
# @param [Hash] prefix
|
466
|
+
# any prefix options to use
|
467
|
+
#
|
468
|
+
# @return [Array<Resource::Base>]
|
469
|
+
# a list of resources in the collection
|
470
|
+
#
|
471
|
+
def collection(prefix = {})
|
472
|
+
connection.get(expanded_collection_path(prefix))
|
473
|
+
end
|
474
|
+
|
475
|
+
#
|
476
|
+
# The path to an individual resource.
|
477
|
+
#
|
478
|
+
# @param [Hash] prefix
|
479
|
+
# the list of prefix options
|
480
|
+
#
|
481
|
+
# @return [String]
|
482
|
+
# the path to the resource
|
483
|
+
#
|
484
|
+
def resource_path(id, prefix = {})
|
485
|
+
[expanded_collection_path(prefix), id].join('/')
|
486
|
+
end
|
487
|
+
|
488
|
+
#
|
489
|
+
# Expand the collection path, "interpolating" any parameters. This syntax
|
490
|
+
# is heavily borrowed from Rails and it will make more sense by looking
|
491
|
+
# at an example.
|
492
|
+
#
|
493
|
+
# @example
|
494
|
+
# /bacon, {} #=> "foo"
|
495
|
+
# /bacon/:type, { type: 'crispy' } #=> "bacon/crispy"
|
496
|
+
#
|
497
|
+
# @raise [Error::MissingURLParameter]
|
498
|
+
# if a required parameter is not given
|
499
|
+
#
|
500
|
+
# @param [Hash] prefix
|
501
|
+
# the list of prefix options
|
502
|
+
#
|
503
|
+
# @return [String]
|
504
|
+
# the "interpolated" URL string
|
505
|
+
#
|
506
|
+
def expanded_collection_path(prefix = {})
|
507
|
+
collection_path.gsub(/:\w+/) do |param|
|
508
|
+
key = param.delete(':')
|
509
|
+
value = prefix[key] || prefix[key.to_sym]
|
510
|
+
|
511
|
+
if value.nil?
|
512
|
+
raise Error::MissingURLParameter.new(param: key)
|
513
|
+
end
|
514
|
+
|
515
|
+
URI.escape(value)
|
516
|
+
end.sub(/^\//, '') # Remove leading slash
|
517
|
+
end
|
518
|
+
|
519
|
+
#
|
520
|
+
# The current connection object.
|
521
|
+
#
|
522
|
+
# @return [ChefAPI::Connection]
|
523
|
+
#
|
524
|
+
def connection
|
525
|
+
Thread.current['chefapi.connection'] || ChefAPI.connection
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
#
|
530
|
+
# The list of associations.
|
531
|
+
#
|
532
|
+
# @return [Hash]
|
533
|
+
#
|
534
|
+
attr_reader :associations
|
535
|
+
|
536
|
+
#
|
537
|
+
# Initialize a new resource with the given attributes. These attributes
|
538
|
+
# are merged with the default values from the schema. Any attributes
|
539
|
+
# that aren't defined in the schema are silently ignored for security
|
540
|
+
# purposes.
|
541
|
+
#
|
542
|
+
# @example create a resource using attributes
|
543
|
+
# Bacon.new(foo: 'bar', zip: 'zap') #=> #<ChefAPI::Resource::Bacon>
|
544
|
+
#
|
545
|
+
# @example using a block
|
546
|
+
# Bacon.new do |bacon|
|
547
|
+
# bacon.foo = 'bar'
|
548
|
+
# bacon.zip = 'zap'
|
549
|
+
# end
|
550
|
+
#
|
551
|
+
# @param [Hash] attributes
|
552
|
+
# the list of initial attributes to set on the model
|
553
|
+
# @param [Hash] prefix
|
554
|
+
# the list of prefix options (for nested resources)
|
555
|
+
#
|
556
|
+
def initialize(attributes = {}, prefix = {})
|
557
|
+
@schema = self.class.schema.dup
|
558
|
+
@schema.load_flavor(self.class.connection.flavor)
|
559
|
+
|
560
|
+
@associations = {}
|
561
|
+
@_prefix = prefix
|
562
|
+
|
563
|
+
# Define a getter and setter method for each attribute in the schema
|
564
|
+
_attributes.each do |key, value|
|
565
|
+
define_singleton_method(key) { _attributes[key] }
|
566
|
+
define_singleton_method("#{key}=") { |value| update_attribute(key, value) }
|
567
|
+
end
|
568
|
+
|
569
|
+
attributes.each do |key, value|
|
570
|
+
unless ignore_attribute?(key)
|
571
|
+
update_attribute(key, value)
|
572
|
+
end
|
573
|
+
end
|
574
|
+
|
575
|
+
yield self if block_given?
|
576
|
+
end
|
577
|
+
|
578
|
+
#
|
579
|
+
# The primary key for the resource.
|
580
|
+
#
|
581
|
+
# @return [Symbol]
|
582
|
+
# the primary key for this resource
|
583
|
+
#
|
584
|
+
def primary_key
|
585
|
+
@schema.primary_key
|
586
|
+
end
|
587
|
+
|
588
|
+
#
|
589
|
+
# The unique id for this resource.
|
590
|
+
#
|
591
|
+
# @return [Object]
|
592
|
+
#
|
593
|
+
def id
|
594
|
+
_attributes[primary_key]
|
595
|
+
end
|
596
|
+
|
597
|
+
#
|
598
|
+
# @todo doc
|
599
|
+
#
|
600
|
+
def _prefix
|
601
|
+
@_prefix
|
602
|
+
end
|
603
|
+
|
604
|
+
#
|
605
|
+
# The list of attributes on this resource.
|
606
|
+
#
|
607
|
+
# @return [Hash<Symbol, Object>]
|
608
|
+
#
|
609
|
+
def _attributes
|
610
|
+
@_attributes ||= {}.merge(@schema.attributes)
|
611
|
+
end
|
612
|
+
|
613
|
+
#
|
614
|
+
# Determine if this resource has the given attribute.
|
615
|
+
#
|
616
|
+
# @param [Symbol, String] key
|
617
|
+
# the attribute key to find
|
618
|
+
#
|
619
|
+
# @return [Boolean]
|
620
|
+
# true if the attribute exists, false otherwise
|
621
|
+
#
|
622
|
+
def attribute?(key)
|
623
|
+
_attributes.has_key?(key.to_sym)
|
624
|
+
end
|
625
|
+
|
626
|
+
#
|
627
|
+
# Determine if this current resource is protected. Resources may be
|
628
|
+
# protected by name or by a Proc. A protected resource is one that should
|
629
|
+
# not be modified (i.e. created/updated/deleted) by the user. An example of
|
630
|
+
# a protected resource is the pivotal key or the chef-webui client.
|
631
|
+
#
|
632
|
+
# @return [Boolean]
|
633
|
+
#
|
634
|
+
def protected?
|
635
|
+
@protected ||= self.class.protected_resources.any? do |thing|
|
636
|
+
if thing.is_a?(Proc)
|
637
|
+
thing.call(self)
|
638
|
+
else
|
639
|
+
id == thing
|
640
|
+
end
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
#
|
645
|
+
# Reload (or reset) this object using the values currently stored on the
|
646
|
+
# remote server. This method will also clear any cached collection proxies
|
647
|
+
# so they will be reloaded the next time they are requested. If the remote
|
648
|
+
# record does not exist, no attributes are modified.
|
649
|
+
#
|
650
|
+
# @note This will remove any custom values you have set on the resource!
|
651
|
+
#
|
652
|
+
# @return [self]
|
653
|
+
# the instance of the reloaded record
|
654
|
+
#
|
655
|
+
def reload!
|
656
|
+
associations.clear
|
657
|
+
|
658
|
+
remote = self.class.fetch(id, _prefix)
|
659
|
+
return self if remote.nil?
|
660
|
+
|
661
|
+
remote._attributes.each do |key, value|
|
662
|
+
update_attribute(key, value)
|
663
|
+
end
|
664
|
+
|
665
|
+
self
|
666
|
+
end
|
667
|
+
|
668
|
+
#
|
669
|
+
# Commit the resource and any changes to the remote Chef Server. Any errors
|
670
|
+
# will raise an exception in the main thread and the resource will not be
|
671
|
+
# committed back to the Chef Server.
|
672
|
+
#
|
673
|
+
# Any response errors (such as server-side responses) that ChefAPI failed
|
674
|
+
# to account for in validations will also raise an exception.
|
675
|
+
#
|
676
|
+
# @return [Boolean]
|
677
|
+
# true if the resource was saved
|
678
|
+
#
|
679
|
+
def save!
|
680
|
+
validate!
|
681
|
+
|
682
|
+
response = if new_resource?
|
683
|
+
self.class.post(to_json, _prefix)
|
684
|
+
else
|
685
|
+
self.class.put(id, to_json, _prefix)
|
686
|
+
end
|
687
|
+
|
688
|
+
# Update our local copy with any partial information that was returned
|
689
|
+
# from the server, ignoring an "bad" attributes that aren't defined in
|
690
|
+
# our schema.
|
691
|
+
response.each do |key, value|
|
692
|
+
update_attribute(key, value) if attribute?(key)
|
693
|
+
end
|
694
|
+
|
695
|
+
true
|
696
|
+
end
|
697
|
+
|
698
|
+
#
|
699
|
+
# Commit the resource and any changes to the remote Chef Server. Any errors
|
700
|
+
# are gracefully handled and added to the resource's error collection for
|
701
|
+
# handling.
|
702
|
+
#
|
703
|
+
# @return [Boolean]
|
704
|
+
# true if the save was successfuly, false otherwise
|
705
|
+
#
|
706
|
+
def save
|
707
|
+
save!
|
708
|
+
rescue
|
709
|
+
false
|
710
|
+
end
|
711
|
+
|
712
|
+
#
|
713
|
+
# Remove the resource from the Chef Server.
|
714
|
+
#
|
715
|
+
# @return [self]
|
716
|
+
# the current instance of this object
|
717
|
+
#
|
718
|
+
def destroy
|
719
|
+
self.class.delete(id, _prefix)
|
720
|
+
self
|
721
|
+
end
|
722
|
+
|
723
|
+
#
|
724
|
+
# Update a subset of attributes on the current resource. This is a handy
|
725
|
+
# way to update multiple attributes at once.
|
726
|
+
#
|
727
|
+
# @param [Hash] attributes
|
728
|
+
# the list of attributes to update
|
729
|
+
#
|
730
|
+
# @return [self]
|
731
|
+
#
|
732
|
+
def update(attributes = {})
|
733
|
+
attributes.each do |key, value|
|
734
|
+
update_attribute(key, value)
|
735
|
+
end
|
736
|
+
|
737
|
+
self
|
738
|
+
end
|
739
|
+
|
740
|
+
#
|
741
|
+
# Update a single attribute in the attributes hash.
|
742
|
+
#
|
743
|
+
# @raise
|
744
|
+
#
|
745
|
+
def update_attribute(key, value)
|
746
|
+
unless attribute?(key.to_sym)
|
747
|
+
raise Error::UnknownAttribute.new(attribute: key)
|
748
|
+
end
|
749
|
+
|
750
|
+
_attributes[key.to_sym] = value
|
751
|
+
end
|
752
|
+
|
753
|
+
#
|
754
|
+
# The list of validators for this resource. This is primarily set and
|
755
|
+
# managed by the underlying schema clean room.
|
756
|
+
#
|
757
|
+
# @return [Array<~Validator::Base>]
|
758
|
+
# the list of validators for this resource
|
759
|
+
#
|
760
|
+
def validators
|
761
|
+
@validators ||= @schema.validators
|
762
|
+
end
|
763
|
+
|
764
|
+
#
|
765
|
+
# Run all of this resource's validations, raising an exception if any
|
766
|
+
# validations fail.
|
767
|
+
#
|
768
|
+
# @raise [Error::InvalidResource]
|
769
|
+
# if any of the validations fail
|
770
|
+
#
|
771
|
+
# @return [Boolean]
|
772
|
+
# true if the validation was successful - this method will never return
|
773
|
+
# anything other than true because an exception is raised if validations
|
774
|
+
# fail
|
775
|
+
#
|
776
|
+
def validate!
|
777
|
+
unless valid?
|
778
|
+
sentence = errors.full_messages.join(', ')
|
779
|
+
raise Error::InvalidResource.new(errors: sentence)
|
780
|
+
end
|
781
|
+
|
782
|
+
true
|
783
|
+
end
|
784
|
+
|
785
|
+
#
|
786
|
+
# Determine if the current resource is valid. This relies on the
|
787
|
+
# validations defined in the schema at initialization.
|
788
|
+
#
|
789
|
+
# @return [Boolean]
|
790
|
+
# true if the resource is valid, false otherwise
|
791
|
+
#
|
792
|
+
def valid?
|
793
|
+
errors.clear
|
794
|
+
|
795
|
+
validators.each do |validator|
|
796
|
+
validator.validate(self)
|
797
|
+
end
|
798
|
+
|
799
|
+
errors.empty?
|
800
|
+
end
|
801
|
+
|
802
|
+
#
|
803
|
+
# Check if this resource exists on the remote Chef Server. This is useful
|
804
|
+
# when determining if a resource should be saved or updated, since a
|
805
|
+
# resource must exist before it can be saved.
|
806
|
+
#
|
807
|
+
# @example when the resource does not exist on the remote Chef Server
|
808
|
+
# bacon = Bacon.new
|
809
|
+
# bacon.new_resource? #=> true
|
810
|
+
#
|
811
|
+
# @example when the resource exists on the remote Chef Server
|
812
|
+
# bacon = Bacon.first
|
813
|
+
# bacon.new_resource? #=> false
|
814
|
+
#
|
815
|
+
# @return [Boolean]
|
816
|
+
# true if the resource exists on the remote Chef Server, false otherwise
|
817
|
+
#
|
818
|
+
def new_resource?
|
819
|
+
!self.class.exists?(id, _prefix)
|
820
|
+
end
|
821
|
+
|
822
|
+
#
|
823
|
+
# Check if the local resource is in sync with the remote Chef Server. When
|
824
|
+
# a remote resource is updated, ChefAPI has no way of knowing it's cached
|
825
|
+
# resources are dirty unless additional requests are made against the
|
826
|
+
# remote Chef Server and diffs are compared.
|
827
|
+
#
|
828
|
+
# @example when the resource is out of sync with the remote Chef Server
|
829
|
+
# bacon = Bacon.first
|
830
|
+
# bacon.description = "I'm different, yeah, I'm different!"
|
831
|
+
# bacon.dirty? #=> true
|
832
|
+
#
|
833
|
+
# @example when the resource is in sync with the remote Chef Server
|
834
|
+
# bacon = Bacon.first
|
835
|
+
# bacon.dirty? #=> false
|
836
|
+
#
|
837
|
+
# @return [Boolean]
|
838
|
+
# true if the local resource has differing attributes from the same
|
839
|
+
# resource on the remote Chef Server, false otherwise
|
840
|
+
#
|
841
|
+
def dirty?
|
842
|
+
new_resource? || !diff.empty?
|
843
|
+
end
|
844
|
+
|
845
|
+
#
|
846
|
+
# Calculate a differential of the attributes on the local resource with
|
847
|
+
# it's remote Chef Server counterpart.
|
848
|
+
#
|
849
|
+
# @example when the local resource is in sync with the remote resource
|
850
|
+
# bacon = Bacon.first
|
851
|
+
# bacon.diff #=> {}
|
852
|
+
#
|
853
|
+
# @example when the local resource differs from the remote resource
|
854
|
+
# bacon = Bacon.first
|
855
|
+
# bacon.description = "My new description"
|
856
|
+
# bacon.diff #=> { :description => { :local => "My new description", :remote => "Old description" } }
|
857
|
+
#
|
858
|
+
# @note This is a VERY expensive operation - use it sparringly!
|
859
|
+
#
|
860
|
+
# @return [Hash]
|
861
|
+
#
|
862
|
+
def diff
|
863
|
+
diff = {}
|
864
|
+
|
865
|
+
remote = self.class.fetch(id, _prefix) || self.class.new({}, _prefix)
|
866
|
+
remote._attributes.each do |key, value|
|
867
|
+
unless _attributes[key] == value
|
868
|
+
diff[key] = { local: _attributes[key], remote: value }
|
869
|
+
end
|
870
|
+
end
|
871
|
+
|
872
|
+
diff
|
873
|
+
end
|
874
|
+
|
875
|
+
#
|
876
|
+
# The URL for this resource on the Chef Server.
|
877
|
+
#
|
878
|
+
# @example Get the resource path for a resource
|
879
|
+
# bacon = Bacon.first
|
880
|
+
# bacon.resource_path #=> /bacons/crispy
|
881
|
+
#
|
882
|
+
# @return [String]
|
883
|
+
# the partial URL path segment
|
884
|
+
#
|
885
|
+
def resource_path
|
886
|
+
self.class.resource_path(id, _prefix)
|
887
|
+
end
|
888
|
+
|
889
|
+
#
|
890
|
+
# Determine if a given attribute should be ignored. Ignored attributes
|
891
|
+
# are defined at the schema level and are frozen.
|
892
|
+
#
|
893
|
+
# @param [Symbol] key
|
894
|
+
# the attribute to check ignorance
|
895
|
+
#
|
896
|
+
# @return [Boolean]
|
897
|
+
#
|
898
|
+
def ignore_attribute?(key)
|
899
|
+
@schema.ignored_attributes.has_key?(key.to_sym)
|
900
|
+
end
|
901
|
+
|
902
|
+
#
|
903
|
+
# The collection of errors on the resource.
|
904
|
+
#
|
905
|
+
# @return [ErrorCollection]
|
906
|
+
#
|
907
|
+
def errors
|
908
|
+
@errors ||= ErrorCollection.new
|
909
|
+
end
|
910
|
+
|
911
|
+
#
|
912
|
+
# The hash representation of this resource. All attributes are serialized
|
913
|
+
# and any values that respond to +to_hash+ are also serialized.
|
914
|
+
#
|
915
|
+
# @return [Hash]
|
916
|
+
#
|
917
|
+
def to_hash
|
918
|
+
{}.tap do |hash|
|
919
|
+
_attributes.each do |key, value|
|
920
|
+
hash[key] = value.respond_to?(:to_hash) ? value.to_hash : value
|
921
|
+
end
|
922
|
+
end
|
923
|
+
end
|
924
|
+
|
925
|
+
#
|
926
|
+
# The JSON serialization of this resource.
|
927
|
+
#
|
928
|
+
# @return [String]
|
929
|
+
#
|
930
|
+
def to_json(*)
|
931
|
+
JSON.fast_generate(to_hash)
|
932
|
+
end
|
933
|
+
|
934
|
+
#
|
935
|
+
# Custom to_s method for easier readability.
|
936
|
+
#
|
937
|
+
# @return [String]
|
938
|
+
#
|
939
|
+
def to_s
|
940
|
+
"#<#{self.class.classname} #{primary_key}: #{id.inspect}>"
|
941
|
+
end
|
942
|
+
|
943
|
+
#
|
944
|
+
# Custom inspect method for easier readability.
|
945
|
+
#
|
946
|
+
# @return [String]
|
947
|
+
#
|
948
|
+
def inspect
|
949
|
+
attrs = (_prefix).merge(_attributes).map do |key, value|
|
950
|
+
if value.is_a?(String)
|
951
|
+
"#{key}: #{Util.truncate(value, length: 50).inspect}"
|
952
|
+
else
|
953
|
+
"#{key}: #{value.inspect}"
|
954
|
+
end
|
955
|
+
end
|
956
|
+
|
957
|
+
"#<#{self.class.classname} #{attrs.join(', ')}>"
|
958
|
+
end
|
959
|
+
end
|
960
|
+
end
|