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,84 @@
1
+ module ChefAPI
2
+ class Resource::Client < Resource::Base
3
+ include ChefAPI::AclAble
4
+ collection_path '/clients'
5
+
6
+ schema do
7
+ attribute :name, type: String, primary: true, required: true
8
+ attribute :admin, type: Boolean, default: false
9
+ attribute :public_key, type: String
10
+ attribute :private_key, type: [String, Boolean], default: false
11
+ attribute :validator, type: Boolean, default: false
12
+
13
+ ignore :certificate, :clientname, :orgname
14
+ end
15
+
16
+ # @todo implement
17
+ protect 'chef-webui', 'chef-validator'
18
+
19
+ class << self
20
+ #
21
+ # Load the client from a .pem file on disk. Lots of assumptions are made
22
+ # here.
23
+ #
24
+ # @param [String] path
25
+ # the path to the client on disk
26
+ #
27
+ # @return [Resource::Client]
28
+ #
29
+ def from_file(path)
30
+ name, key = Util.safe_read(path)
31
+
32
+ if client = fetch(name)
33
+ client.private_key = key
34
+ client
35
+ else
36
+ new(name: name, private_key: key)
37
+ end
38
+ end
39
+ end
40
+
41
+ #
42
+ # Override the loading of the client. Since HEC and EC both return
43
+ # +certificate+, but OPC and CZ both use +public_key+. In order to
44
+ # normalize this discrepancy, the intializer converts the response from
45
+ # the server OPC format. HEC and EC both handle putting a public key to
46
+ # the server instead of a certificate.
47
+ #
48
+ # @see Resource::Base#initialize
49
+ #
50
+ def initialize(attributes = {}, prefix = {})
51
+ if certificate = attributes.delete(:certificate) ||
52
+ attributes.delete('certificate')
53
+ x509 = OpenSSL::X509::Certificate.new(certificate)
54
+ attributes[:public_key] = x509.public_key.to_pem
55
+ end
56
+
57
+ super
58
+ end
59
+
60
+ #
61
+ # Generate a new RSA private key for this API client.
62
+ #
63
+ # @example Regenerate the private key
64
+ # key = client.regenerate_key
65
+ # key #=> "-----BEGIN PRIVATE KEY-----\nMIGfMA0GCS..."
66
+ #
67
+ # @note For security reasons, you should perform this operation sparingly!
68
+ # The resulting private key is committed to this object, meaning it is
69
+ # saved to memory somewhere. You should set this resource's +private_key+
70
+ # to +nil+ after you have committed it to disk and perform a manual GC to
71
+ # be ultra-secure.
72
+ #
73
+ # @note Regenerating the private key also regenerates the public key!
74
+ #
75
+ # @return [self]
76
+ # the current resource with the new public and private key attributes
77
+ #
78
+ def regenerate_keys
79
+ raise Error::CannotRegenerateKey if new_resource?
80
+ update(private_key: true).save!
81
+ self
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,234 @@
1
+ module ChefAPI
2
+ class Resource::CollectionProxy
3
+ include Enumerable
4
+
5
+ #
6
+ # Create a new collection proxy from the given parent class and collection
7
+ # information. The collection proxy aims to make working with nested
8
+ # resource collections a bit easier. The proxy waits until non-existing
9
+ # data is requested before making another HTTP request. In this way, it
10
+ # helps reduce bandwidth and API requets.
11
+ #
12
+ # Additionally, the collection proxy caches the results of an object request
13
+ # in memory by +id+, so additional requests for the same object will hit the
14
+ # cache, again reducing HTTP requests.
15
+ #
16
+ # @param [Resource::Base] parent
17
+ # the parent resource that created the collection
18
+ # @param [Class] klass
19
+ # the class the resulting objects should be
20
+ # @param [String] endpoint
21
+ # the relative path for the RESTful endpoint
22
+ #
23
+ # @return [CollectionProxy]
24
+ #
25
+ def initialize(parent, klass, endpoint, prefix = {})
26
+ @parent = parent
27
+ @klass = klass
28
+ @endpoint = "#{parent.resource_path}/#{endpoint}"
29
+ @prefix = prefix
30
+ @collection = load_collection
31
+ end
32
+
33
+ #
34
+ # Force a reload of this collection proxy and all its elements. This is
35
+ # useful if you think additional items have been added to the remote
36
+ # collection and need access to them locally. This will also clear any
37
+ # existing cached responses, so use with caution.
38
+ #
39
+ # @return [self]
40
+ #
41
+ def reload!
42
+ cache.clear
43
+ @collection = load_collection
44
+
45
+ self
46
+ end
47
+
48
+ #
49
+ # Fetch a specific resource in the collection by id.
50
+ #
51
+ # @example Fetch a resource
52
+ # Bacon.first.items.fetch('crispy')
53
+ #
54
+ # @param [String, Symbol] id
55
+ # the id of the resource to fetch
56
+ #
57
+ # @return [Resource::Base, nil]
58
+ # the fetched class, or nil if it does not exists
59
+ #
60
+ def fetch(id)
61
+ return nil unless exists?(id)
62
+ cached(id) { klass.from_url(get(id), prefix) }
63
+ end
64
+
65
+ #
66
+ # Determine if the resource with the given id exists on the remote Chef
67
+ # Server. This method does not actually query the Chef Server, but rather
68
+ # delegates to the cached collection. To guarantee the most fresh set of
69
+ # data, you should call +reload!+ before +exists?+ to ensure you have the
70
+ # most up-to-date collection of resources.
71
+ #
72
+ # @param [String, Symbol] id
73
+ # the unique id of the resource to find.
74
+ #
75
+ # @return [Boolean]
76
+ # true if the resource exists, false otherwise
77
+ #
78
+ def exists?(id)
79
+ collection.has_key?(id.to_s)
80
+ end
81
+
82
+ #
83
+ # Get the full list of all entries in the collection. This method is
84
+ # incredibly expensive and should only be used when you absolutely need
85
+ # all resources. If you need a specific resource, you should use an iterator
86
+ # such as +select+ or +find+ instead, since it will minimize HTTP requests.
87
+ # Once all the objects are requested, they are cached, reducing the number
88
+ # of HTTP requests.
89
+ #
90
+ # @return [Array<Resource::Base>]
91
+ #
92
+ def all
93
+ entries
94
+ end
95
+
96
+ #
97
+ # The custom iterator for looping over each object in this collection. For
98
+ # more information, please see the +Enumerator+ module in Ruby core.
99
+ #
100
+ def each(&block)
101
+ collection.each do |id, url|
102
+ object = cached(id) { klass.from_url(url, prefix) }
103
+ block.call(object) if block
104
+ end
105
+ end
106
+
107
+ #
108
+ # The total number of items in this collection. This method does not make
109
+ # an API request, but rather counts the number of keys in the given
110
+ # collection.
111
+ #
112
+ def count
113
+ collection.length
114
+ end
115
+ alias_method :size, :count
116
+
117
+ #
118
+ # The string representation of this collection proxy.
119
+ #
120
+ # @return [String]
121
+ #
122
+ def to_s
123
+ "#<#{self.class.name}>"
124
+ end
125
+
126
+ #
127
+ # The detailed string representation of this collection proxy.
128
+ #
129
+ # @return [String]
130
+ #
131
+ def inspect
132
+ objects = collection
133
+ .map { |id, _| cached(id) || klass.new(klass.schema.primary_key => id) }
134
+ .map { |object| object.to_s }
135
+
136
+ "#<#{self.class.name} [#{objects.join(', ')}]>"
137
+ end
138
+
139
+ private
140
+
141
+ attr_reader :collection
142
+ attr_reader :endpoint
143
+ attr_reader :klass
144
+ attr_reader :parent
145
+ attr_reader :prefix
146
+
147
+ #
148
+ # Fetch the object collection from the Chef Server. Since the Chef Server's
149
+ # API is completely insane and all over the place, it might return a Hash
150
+ # where the key is the id of the resource and the value is the url for that
151
+ # item on the Chef Server:
152
+ #
153
+ # { "key" => "url" }
154
+ #
155
+ # Or if the Chef Server's fancy is tickled, it might just return an array
156
+ # of the list of items:
157
+ #
158
+ # ["item_1", "item_2"]
159
+ #
160
+ # Or if the Chef Server is feeling especially magical, it might return the
161
+ # actual objects, but prefixed with the JSON id:
162
+ #
163
+ # [{"organization" => {"_id" => "..."}}, {"organization" => {...}}]
164
+ #
165
+ # So, this method attempts to intelligent handle these use cases. That being
166
+ # said, I can almost guarantee that someone is going to do some crazy
167
+ # strange edge case with this library and hit a bug here, so it will likely
168
+ # be changed in the future. For now, it "works on my machine".
169
+ #
170
+ # @return [Hash]
171
+ #
172
+ def load_collection
173
+ case response = Resource::Base.connection.get(endpoint)
174
+ when Array
175
+ if response.first.is_a?(Hash)
176
+ key = klass.schema.primary_key.to_s
177
+
178
+ {}.tap do |hash|
179
+ response.each do |results|
180
+ results.each do |_, info|
181
+ hash[key] = klass.resource_path(info[key])
182
+ end
183
+ end
184
+ end
185
+ else
186
+ Hash[*response.map { |item| [item, klass.resource_path(item)] }.flatten]
187
+ end
188
+ when Hash
189
+ response
190
+ end
191
+ end
192
+
193
+ #
194
+ # Retrieve a cached value. This method helps significantly reduce the
195
+ # number of HTTP requests made against the remote server.
196
+ #
197
+ # @param [String, Symbol] key
198
+ # the cache key (typically the +name+ of the resource)
199
+ # @param [Proc] block
200
+ # the block to evaluate to set the value if it doesn't exist
201
+ #
202
+ # @return [Object]
203
+ # the value at the cache
204
+ #
205
+ def cached(key, &block)
206
+ cache[key.to_sym] ||= block ? block.call : nil
207
+ end
208
+
209
+ #
210
+ # The cache...
211
+ #
212
+ # @return [Hash]
213
+ #
214
+ def cache
215
+ @cache ||= {}
216
+ end
217
+
218
+ #
219
+ # Retrieve a specific item in the collection. Note, this will always return
220
+ # the original raw record (with the key => URL pairing), not a cached
221
+ # resource.
222
+ #
223
+ # @param [String, Symbol] id
224
+ # the id of the resource to fetch
225
+ #
226
+ # @return [String, nil]
227
+ # the URL to retrieve the item in the collection, or nil if it does not
228
+ # exist
229
+ #
230
+ def get(id)
231
+ collection[id.to_s]
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,24 @@
1
+ module ChefAPI
2
+ #
3
+ # In the real world, a "cookbook" is a single entity with multiple versions.
4
+ # In Chef land, a "cookbook" is actually just a wrapper around a collection
5
+ # of +cookbook_version+ objects that fully detail the layout of a cookbook.
6
+ #
7
+ class Resource::Cookbook < Resource::Base
8
+ collection_path '/cookbooks'
9
+
10
+ schema do
11
+ attribute :name, type: String, primary: true, required: true
12
+ end
13
+
14
+ has_many :versions,
15
+ class_name: CookbookVersion,
16
+ rest_endpoint: '/?num_versions=all'
17
+
18
+ class << self
19
+ def from_json(response, prefix = {})
20
+ new(name: response.keys.first)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ module ChefAPI
2
+ class Resource::CookbookVersion < Resource::Base
3
+ collection_path '/cookbooks/:cookbook'
4
+
5
+ schema do
6
+ attribute :name, type: String, primary: true, required: true
7
+ attribute :cookbook_name, type: String, required: true
8
+ attribute :metadata, type: Hash, required: true
9
+ attribute :version, type: String, required: true
10
+ attribute :frozen?, type: Boolean, default: false
11
+
12
+ attribute :attributes, type: Array, default: []
13
+ attribute :definitions, type: Array, default: []
14
+ attribute :files, type: Array, default: []
15
+ attribute :libraries, type: Array, default: []
16
+ attribute :providers, type: Array, default: []
17
+ attribute :recipes, type: Array, default: []
18
+ attribute :resources, type: Array, default: []
19
+ attribute :root_files, type: Array, default: []
20
+ attribute :templates, type: Array, default: []
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,136 @@
1
+ module ChefAPI
2
+ class Resource::DataBag < Resource::Base
3
+ collection_path '/data'
4
+
5
+ schema do
6
+ attribute :name, type: String, primary: true, required: true
7
+ end
8
+
9
+ class << self
10
+ #
11
+ # Load the data bag from a collection of JSON files on disk. Just like
12
+ # +knife+, the basename of the folder is assumed to be the name of the
13
+ # data bag and all containing items a proper JSON data bag.
14
+ #
15
+ # This will load **all** items in the data bag, returning an array of
16
+ # those items. To load an individual data bag item, see
17
+ # {DataBagItem.from_file}.
18
+ #
19
+ # **This method does NOT return an instance of a {DataBag}!**
20
+ #
21
+ # @param [String] path
22
+ # the path to the data bag **folder** on disk
23
+ # @param [String] name
24
+ # the name of the data bag
25
+ #
26
+ # @return [Array<DataBagItem>]
27
+ #
28
+ def from_file(path, name = File.basename(path))
29
+ path = File.expand_path(path)
30
+
31
+ raise Error::FileNotFound.new(path: path) unless File.exists?(path)
32
+ raise Error::NotADirectory.new(path: path) unless File.directory?(path)
33
+
34
+ raise ArgumentError unless File.directory?(path)
35
+
36
+ bag = new(name: name)
37
+
38
+ Util.fast_collect(Dir["#{path}/*.json"]) do |item|
39
+ Resource::DataBagItem.from_file(item, bag)
40
+ end
41
+ end
42
+
43
+ #
44
+ #
45
+ #
46
+ def fetch(id, prefix = {})
47
+ return nil if id.nil?
48
+
49
+ path = resource_path(id, prefix)
50
+ response = connection.get(path)
51
+ new(name: id)
52
+ rescue Error::HTTPNotFound
53
+ nil
54
+ end
55
+
56
+ #
57
+ #
58
+ #
59
+ def each(&block)
60
+ collection.each do |name, path|
61
+ result = new(name: name)
62
+ block.call(result) if block
63
+ end
64
+ end
65
+ end
66
+
67
+ #
68
+ # This is the same as +has_many :items+, but creates a special collection
69
+ # for data bag items, which is mutable and handles some special edge cases
70
+ # that only data bags encounter.
71
+ #
72
+ # @see Base.has_many
73
+ #
74
+ def items
75
+ associations[:items] ||= Resource::DataBagItemCollectionProxy.new(self)
76
+ end
77
+ end
78
+ end
79
+
80
+ module ChefAPI
81
+ #
82
+ # The mutable collection is a special kind of collection proxy that permits
83
+ # Rails-like attribtue creation, like:
84
+ #
85
+ # DataBag.first.items.create(id: 'me', thing: 'bar', zip: 'zap')
86
+ #
87
+ class Resource::DataBagItemCollectionProxy < Resource::CollectionProxy
88
+ def initialize(bag)
89
+ # Delegate to the superclass
90
+ super(bag, Resource::DataBagItem, nil, bag: bag.name)
91
+ end
92
+
93
+ # @see klass.new
94
+ def new(data = {})
95
+ klass.new(data, prefix, parent)
96
+ end
97
+
98
+ # @see klass.destroy
99
+ def destroy(id)
100
+ klass.destroy(id, prefix)
101
+ ensure
102
+ reload!
103
+ end
104
+
105
+ # @see klass.destroy_all
106
+ def destroy_all
107
+ klass.destroy_all(prefix)
108
+ ensure
109
+ reload!
110
+ end
111
+
112
+ # @see klass.build
113
+ def build(data = {})
114
+ klass.build(data, prefix)
115
+ end
116
+
117
+ # @see klass.create
118
+ def create(data = {})
119
+ klass.create(data, prefix)
120
+ ensure
121
+ reload!
122
+ end
123
+
124
+ # @see klass.create!
125
+ def create!(data = {})
126
+ klass.create!(data, prefix)
127
+ ensure
128
+ reload!
129
+ end
130
+
131
+ # @see klass.update
132
+ def update(id, data = {})
133
+ klass.update(id, data, prefix)
134
+ end
135
+ end
136
+ end