chef-api 0.2.0

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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.travis.yml +14 -0
  4. data/Gemfile +12 -0
  5. data/LICENSE +201 -0
  6. data/README.md +264 -0
  7. data/Rakefile +1 -0
  8. data/chef-api.gemspec +25 -0
  9. data/lib/chef-api/boolean.rb +6 -0
  10. data/lib/chef-api/configurable.rb +78 -0
  11. data/lib/chef-api/connection.rb +466 -0
  12. data/lib/chef-api/defaults.rb +130 -0
  13. data/lib/chef-api/error_collection.rb +44 -0
  14. data/lib/chef-api/errors.rb +35 -0
  15. data/lib/chef-api/logger.rb +160 -0
  16. data/lib/chef-api/proxy.rb +72 -0
  17. data/lib/chef-api/resource.rb +16 -0
  18. data/lib/chef-api/resources/base.rb +951 -0
  19. data/lib/chef-api/resources/client.rb +85 -0
  20. data/lib/chef-api/resources/collection_proxy.rb +217 -0
  21. data/lib/chef-api/resources/cookbook.rb +24 -0
  22. data/lib/chef-api/resources/cookbook_version.rb +23 -0
  23. data/lib/chef-api/resources/data_bag.rb +136 -0
  24. data/lib/chef-api/resources/data_bag_item.rb +35 -0
  25. data/lib/chef-api/resources/environment.rb +16 -0
  26. data/lib/chef-api/resources/node.rb +17 -0
  27. data/lib/chef-api/resources/principal.rb +11 -0
  28. data/lib/chef-api/resources/role.rb +16 -0
  29. data/lib/chef-api/resources/user.rb +11 -0
  30. data/lib/chef-api/schema.rb +112 -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/lib/chef-api.rb +76 -0
  38. data/locales/en.yml +89 -0
  39. data/spec/integration/resources/client_spec.rb +8 -0
  40. data/spec/integration/resources/environment_spec.rb +8 -0
  41. data/spec/integration/resources/node_spec.rb +8 -0
  42. data/spec/integration/resources/role_spec.rb +8 -0
  43. data/spec/spec_helper.rb +26 -0
  44. data/spec/support/chef_server.rb +115 -0
  45. data/spec/support/shared/chef_api_resource.rb +91 -0
  46. data/spec/unit/resources/base_spec.rb +47 -0
  47. data/spec/unit/resources/client_spec.rb +69 -0
  48. metadata +128 -0
@@ -0,0 +1,85 @@
1
+ module ChefAPI
2
+ class Resource::Client < Resource::Base
3
+ collection_path '/clients'
4
+
5
+ schema do
6
+ attribute :name, type: String, primary: true, required: true
7
+ attribute :admin, type: Boolean, default: false
8
+ attribute :public_key, type: String
9
+ attribute :private_key, type: [String, Boolean], default: false
10
+ attribute :validator, type: Boolean, default: false
11
+
12
+ ignore :certificate, :clientname, :orgname
13
+ end
14
+
15
+ # @todo implement
16
+ protect 'chef-webui', 'chef-validator'
17
+
18
+ class << self
19
+ #
20
+ # Load the client from a .pem file on disk. Lots of assumptions are made
21
+ # here.
22
+ #
23
+ # @param [String] path
24
+ # the path to the client on disk
25
+ #
26
+ # @return [Resource::Client]
27
+ #
28
+ def from_file(path)
29
+ name, contents = Util.safe_read(path)
30
+
31
+ if client = fetch(name)
32
+ client.private_key = key
33
+ client
34
+ else
35
+ new(name: name, private_key: key)
36
+ end
37
+ end
38
+ end
39
+
40
+ #
41
+ # Override the loading of the client. Since HEC and EC both return
42
+ # +certificate+, but OPC and CZ both use +public_key+. In order to
43
+ # normalize this discrepancy, the intializer converts the response from
44
+ # the server OPC format. HEC and EC both handle putting a public key to
45
+ # the server instead of a certificate.
46
+ #
47
+ # @see Resource::Base#initialize
48
+ #
49
+ def initialize(attributes = {}, prefix = {})
50
+ if certificate = attributes.delete(:certificate) ||
51
+ attributes.delete('certificate')
52
+ x509 = OpenSSL::X509::Certificate.new(certificate)
53
+ attributes[:public_key] = x509.public_key.to_pem
54
+ end
55
+
56
+ super
57
+ end
58
+
59
+ #
60
+ # Generate a new RSA private key for this API client.
61
+ #
62
+ # @example Regenerate the private key
63
+ # key = client.regenerate_key
64
+ # key #=> "-----BEGIN PRIVATE KEY-----\nMIGfMA0GCS..."
65
+ #
66
+ # @warn
67
+ # For security reasons, you should perform this operation sparingly! The
68
+ # resulting private key is committed to this object, meaning it is saved
69
+ # to memory somewhere. You should set this resource's +private_key+ to
70
+ # +nil+ after you have committed it to disk and perform a manual GC to
71
+ # be ultra-secure.
72
+ #
73
+ # @warn
74
+ # Regenerating the private key also regenerates the public key!
75
+ #
76
+ # @return [self]
77
+ # the current resource with the new public and private key attributes
78
+ #
79
+ def regenerate_keys
80
+ raise Error::CannotRegenerateKey if new_resource?
81
+ update(private_key: true).save!
82
+ self
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,217 @@
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
+ # So, this method attempts to intelligent handle these use cases. That being
161
+ # said, I can almost guarantee that someone is going to do some crazy
162
+ # strange edge case with this library and hit a bug here, so it will likely
163
+ # be changed in the future. For now, it "works on my machine".
164
+ #
165
+ # @return [Hash]
166
+ #
167
+ def load_collection
168
+ case response = ChefAPI.connection.get(endpoint)
169
+ when Array
170
+ Hash[*response.map { |item| [item, klass.resource_path(item)] }.flatten]
171
+ when Hash
172
+ response
173
+ end
174
+ end
175
+
176
+ #
177
+ # Retrieve a cached value. This method helps significantly reduce the
178
+ # number of HTTP requests made against the remote server.
179
+ #
180
+ # @param [String, Symbol] key
181
+ # the cache key (typically the +name+ of the resource)
182
+ # @param [Proc] block
183
+ # the block to evaluate to set the value if it doesn't exist
184
+ #
185
+ # @return [Object]
186
+ # the value at the cache
187
+ #
188
+ def cached(key, &block)
189
+ cache[key.to_sym] ||= block ? block.call : nil
190
+ end
191
+
192
+ #
193
+ # The cache...
194
+ #
195
+ # @return [Hash]
196
+ #
197
+ def cache
198
+ @cache ||= {}
199
+ end
200
+
201
+ #
202
+ # Retrieve a specific item in the collection. Note, this will always return
203
+ # the original raw record (with the key => URL pairing), not a cached
204
+ # resource.
205
+ #
206
+ # @param [String, Symbol] id
207
+ # the id of the resource to fetch
208
+ #
209
+ # @return [String, nil]
210
+ # the URL to retrieve the item in the collection, or nil if it does not
211
+ # exist
212
+ #
213
+ def get(id)
214
+ collection[id.to_s]
215
+ end
216
+ end
217
+ 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
+ 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 = ChefAPI.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
@@ -0,0 +1,35 @@
1
+ module ChefAPI
2
+ class Resource::DataBagItem < Resource::Base
3
+ collection_path '/data/:bag'
4
+
5
+ schema do
6
+ attribute :id, type: String, primary: true, required: true
7
+ attribute :data, type: Hash, default: {}
8
+ end
9
+
10
+ class << self
11
+ def from_file(path, bag = File.basename(File.dirname(path)))
12
+ id, contents = Util.safe_read(path)
13
+ data = JSON.parse(contents)
14
+ data[:id] = id
15
+
16
+ bag = bag.is_a?(Resource::DataBag) ? bag : Resource::DataBag.new(name: bag)
17
+
18
+ new(data, { bag: bag.name }, bag)
19
+ end
20
+ end
21
+
22
+ attr_reader :bag
23
+
24
+ #
25
+ # Override the initialize method to move any attributes into the +data+
26
+ # hash.
27
+ #
28
+ def initialize(attributes = {}, prefix = {}, bag = nil)
29
+ @bag = bag || Resource::DataBag.fetch(prefix[:bag])
30
+
31
+ id = attributes.delete(:id) || attributes.delete('id')
32
+ super({ id: id, data: attributes }, prefix)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,16 @@
1
+ module ChefAPI
2
+ class Resource::Environment < Resource::Base
3
+ collection_path '/environments'
4
+
5
+ schema do
6
+ attribute :name, type: String, primary: true, required: true
7
+ attribute :description, type: String
8
+ attribute :default_attributes, type: Hash, default: {}
9
+ attribute :override_attributes, type: Hash, default: {}
10
+ attribute :cookbook_versions, type: Hash, default: {}
11
+ end
12
+
13
+ has_many :cookbooks
14
+ has_many :nodes
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ module ChefAPI
2
+ class Resource::Node < Resource::Base
3
+ collection_path '/nodes'
4
+
5
+ schema do
6
+ attribute :name, type: String, primary: true, required: true
7
+ attribute :automatic, type: Hash, default: {}
8
+ attribute :default, type: Hash, default: {}
9
+ attribute :normal, type: Hash, default: {}
10
+ attribute :override, type: Hash, default: {}
11
+ attribute :run_list, type: Array, default: []
12
+
13
+ # Enterprise Chef attributes
14
+ attribute :chef_environment, type: String, default: '_default'
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ module ChefAPI
2
+ class Resource::Principal < Resource::Base
3
+ collection_path '/principals'
4
+
5
+ schema do
6
+ attribute :name, type: String, primary: true, required: true
7
+ attribute :type, type: String
8
+ attribute :public_key, type: String
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ module ChefAPI
2
+ class Resource::Role < Resource::Base
3
+ collection_path '/roles'
4
+
5
+ schema do
6
+ attribute :name, type: String, primary: true, required: true
7
+ attribute :description, type: String
8
+ attribute :default_attributes, type: Hash, default: {}
9
+ attribute :override_attributes, type: Hash, default: {}
10
+ attribute :run_list, type: Array, default: []
11
+ attribute :env_run_lists, type: Hash, default: {}
12
+ end
13
+
14
+ has_many :environments
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module ChefAPI
2
+ class Resource::User < Resource::Base
3
+ collection_path '/users'
4
+
5
+ schema do
6
+ attribute :name, type: String, primary: true, required: true
7
+ attribute :admin, type: Boolean, default: false
8
+ attribute :public_key, type: String
9
+ end
10
+ end
11
+ end