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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/chef-api.rb +96 -0
  4. data/lib/chef-api/aclable.rb +35 -0
  5. data/lib/chef-api/authentication.rb +300 -0
  6. data/lib/chef-api/boolean.rb +6 -0
  7. data/lib/chef-api/configurable.rb +80 -0
  8. data/lib/chef-api/connection.rb +507 -0
  9. data/lib/chef-api/defaults.rb +197 -0
  10. data/lib/chef-api/error_collection.rb +44 -0
  11. data/lib/chef-api/errors.rb +64 -0
  12. data/lib/chef-api/multipart.rb +164 -0
  13. data/lib/chef-api/resource.rb +21 -0
  14. data/lib/chef-api/resources/base.rb +960 -0
  15. data/lib/chef-api/resources/client.rb +84 -0
  16. data/lib/chef-api/resources/collection_proxy.rb +234 -0
  17. data/lib/chef-api/resources/cookbook.rb +24 -0
  18. data/lib/chef-api/resources/cookbook_version.rb +23 -0
  19. data/lib/chef-api/resources/data_bag.rb +136 -0
  20. data/lib/chef-api/resources/data_bag_item.rb +53 -0
  21. data/lib/chef-api/resources/environment.rb +16 -0
  22. data/lib/chef-api/resources/group.rb +16 -0
  23. data/lib/chef-api/resources/node.rb +20 -0
  24. data/lib/chef-api/resources/organization.rb +22 -0
  25. data/lib/chef-api/resources/partial_search.rb +44 -0
  26. data/lib/chef-api/resources/principal.rb +11 -0
  27. data/lib/chef-api/resources/role.rb +18 -0
  28. data/lib/chef-api/resources/search.rb +47 -0
  29. data/lib/chef-api/resources/user.rb +82 -0
  30. data/lib/chef-api/schema.rb +150 -0
  31. data/lib/chef-api/util.rb +119 -0
  32. data/lib/chef-api/validator.rb +16 -0
  33. data/lib/chef-api/validators/base.rb +82 -0
  34. data/lib/chef-api/validators/required.rb +11 -0
  35. data/lib/chef-api/validators/type.rb +23 -0
  36. data/lib/chef-api/version.rb +3 -0
  37. data/templates/errors/abstract_method.erb +5 -0
  38. data/templates/errors/cannot_regenerate_key.erb +1 -0
  39. data/templates/errors/chef_api_error.erb +1 -0
  40. data/templates/errors/file_not_found.erb +1 -0
  41. data/templates/errors/http_bad_request.erb +3 -0
  42. data/templates/errors/http_forbidden_request.erb +3 -0
  43. data/templates/errors/http_gateway_timeout.erb +3 -0
  44. data/templates/errors/http_method_not_allowed.erb +3 -0
  45. data/templates/errors/http_not_acceptable.erb +3 -0
  46. data/templates/errors/http_not_found.erb +3 -0
  47. data/templates/errors/http_server_unavailable.erb +1 -0
  48. data/templates/errors/http_unauthorized_request.erb +3 -0
  49. data/templates/errors/insufficient_file_permissions.erb +1 -0
  50. data/templates/errors/invalid_resource.erb +1 -0
  51. data/templates/errors/invalid_validator.erb +1 -0
  52. data/templates/errors/missing_url_parameter.erb +1 -0
  53. data/templates/errors/not_a_directory.erb +1 -0
  54. data/templates/errors/resource_already_exists.erb +1 -0
  55. data/templates/errors/resource_not_found.erb +1 -0
  56. data/templates/errors/resource_not_mutable.erb +1 -0
  57. data/templates/errors/unknown_attribute.erb +1 -0
  58. 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