chef-infra-api 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
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