ridley 0.0.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 (54) hide show
  1. data/.gitignore +17 -0
  2. data/.travis.yml +5 -0
  3. data/Gemfile +3 -0
  4. data/Guardfile +20 -0
  5. data/LICENSE +201 -0
  6. data/README.md +273 -0
  7. data/Thorfile +48 -0
  8. data/lib/ridley.rb +48 -0
  9. data/lib/ridley/connection.rb +131 -0
  10. data/lib/ridley/context.rb +25 -0
  11. data/lib/ridley/dsl.rb +58 -0
  12. data/lib/ridley/errors.rb +82 -0
  13. data/lib/ridley/log.rb +10 -0
  14. data/lib/ridley/middleware.rb +19 -0
  15. data/lib/ridley/middleware/chef_auth.rb +45 -0
  16. data/lib/ridley/middleware/chef_response.rb +28 -0
  17. data/lib/ridley/middleware/parse_json.rb +107 -0
  18. data/lib/ridley/resource.rb +305 -0
  19. data/lib/ridley/resources/client.rb +75 -0
  20. data/lib/ridley/resources/cookbook.rb +27 -0
  21. data/lib/ridley/resources/data_bag.rb +75 -0
  22. data/lib/ridley/resources/data_bag_item.rb +186 -0
  23. data/lib/ridley/resources/environment.rb +45 -0
  24. data/lib/ridley/resources/node.rb +34 -0
  25. data/lib/ridley/resources/role.rb +33 -0
  26. data/lib/ridley/version.rb +3 -0
  27. data/ridley.gemspec +39 -0
  28. data/spec/acceptance/client_resource_spec.rb +135 -0
  29. data/spec/acceptance/cookbook_resource_spec.rb +46 -0
  30. data/spec/acceptance/data_bag_item_resource_spec.rb +171 -0
  31. data/spec/acceptance/data_bag_resource_spec.rb +51 -0
  32. data/spec/acceptance/environment_resource_spec.rb +171 -0
  33. data/spec/acceptance/node_resource_spec.rb +218 -0
  34. data/spec/acceptance/role_resource_spec.rb +200 -0
  35. data/spec/fixtures/reset.pem +27 -0
  36. data/spec/spec_helper.rb +25 -0
  37. data/spec/support/each_matcher.rb +12 -0
  38. data/spec/support/shared_examples/ridley_resource.rb +237 -0
  39. data/spec/support/spec_helpers.rb +11 -0
  40. data/spec/unit/ridley/connection_spec.rb +167 -0
  41. data/spec/unit/ridley/errors_spec.rb +34 -0
  42. data/spec/unit/ridley/middleware/chef_auth_spec.rb +14 -0
  43. data/spec/unit/ridley/middleware/chef_response_spec.rb +213 -0
  44. data/spec/unit/ridley/middleware/parse_json_spec.rb +74 -0
  45. data/spec/unit/ridley/resource_spec.rb +214 -0
  46. data/spec/unit/ridley/resources/client_spec.rb +47 -0
  47. data/spec/unit/ridley/resources/cookbook_spec.rb +5 -0
  48. data/spec/unit/ridley/resources/data_bag_item_spec.rb +42 -0
  49. data/spec/unit/ridley/resources/data_bag_spec.rb +15 -0
  50. data/spec/unit/ridley/resources/environment_spec.rb +73 -0
  51. data/spec/unit/ridley/resources/node_spec.rb +5 -0
  52. data/spec/unit/ridley/resources/role_spec.rb +5 -0
  53. data/spec/unit/ridley_spec.rb +32 -0
  54. metadata +451 -0
@@ -0,0 +1,28 @@
1
+ module Ridley
2
+ module Middleware
3
+ # @author Jamie Winsor <jamie@vialstudios.com>
4
+ class ChefResponse < Faraday::Response::Middleware
5
+ class << self
6
+ # Determines if a response from the Chef server was successful
7
+ #
8
+ # @param [Hash] env
9
+ # a faraday request env
10
+ #
11
+ # @return [Boolean]
12
+ def success?(env)
13
+ (200..210).to_a.index(env[:status].to_i) ? true : false
14
+ end
15
+ end
16
+
17
+ def on_complete(env)
18
+ Ridley.log.debug("Handling Chef Response")
19
+ Ridley.log.debug(env)
20
+
21
+ unless self.class.success?(env)
22
+ Ridley.log.debug("Error encounted in Chef Response")
23
+ raise Errors::HTTPError.fabricate(env)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,107 @@
1
+ module Ridley
2
+ module Middleware
3
+ # @author Jamie Winsor <jamie@vialstudios.com>
4
+ class ParseJson < Faraday::Response::Middleware
5
+ JSON_TYPE = 'application/json'.freeze
6
+
7
+ BRACKETS = [
8
+ "[",
9
+ "{"
10
+ ].freeze
11
+
12
+ WHITESPACE = [
13
+ " ",
14
+ "\n",
15
+ "\r",
16
+ "\t"
17
+ ].freeze
18
+
19
+ class << self
20
+ # Takes a string containing JSON and converts it to a Ruby hash
21
+ # symbols for keys
22
+ #
23
+ # @param [String] body
24
+ #
25
+ # @return [Hash]
26
+ def parse(body)
27
+ MultiJson.load(body, symbolize_keys: true)
28
+ end
29
+
30
+ # Extracts the type of the response from the response headers
31
+ # of a Faraday request env. 'text/html' will be returned if no
32
+ # content-type is specified in the response
33
+ #
34
+ # @example
35
+ # env = {
36
+ # :response_headers => {
37
+ # 'content-type' => 'text/html; charset=utf-8'
38
+ # }
39
+ # ...
40
+ # }
41
+ #
42
+ # ParseJson.response_type(env) => 'application/json'
43
+ #
44
+ # @param [Hash] env
45
+ # a Faraday request env
46
+ #
47
+ # @return [String]
48
+ def response_type(env)
49
+ if env[:response_headers][CONTENT_TYPE].nil?
50
+ Ridley.log.debug "Response did not specify a content type."
51
+ return "text/html"
52
+ end
53
+
54
+ env[:response_headers][CONTENT_TYPE].split(';', 2).first
55
+ end
56
+
57
+ # Determines if the response of the given Faraday request env
58
+ # contains JSON
59
+ #
60
+ # @param [Hash] env
61
+ # a Faraday request env
62
+ #
63
+ # @return [Boolean]
64
+ def json_response?(env)
65
+ response_type(env) == JSON_TYPE ||
66
+ looks_like_json?(env)
67
+ end
68
+
69
+ # Examines the body of a request env and returns true if it appears
70
+ # to contain JSON or false if it does not
71
+ #
72
+ # @param [Hash] env
73
+ # a Faraday request env
74
+ # @return [Boolean]
75
+ def looks_like_json?(env)
76
+ return false unless env[:body].present?
77
+
78
+ BRACKETS.include?(first_char(env[:body]))
79
+ end
80
+
81
+ private
82
+
83
+ def first_char(body)
84
+ idx = -1
85
+ begin
86
+ char = body[idx += 1]
87
+ char = char.chr if char
88
+ end while char && WHITESPACE.include?(char)
89
+
90
+ char
91
+ end
92
+ end
93
+
94
+ def on_complete(env)
95
+ if self.class.json_response?(env)
96
+ Ridley.log.debug("Parsing JSON Chef Response")
97
+ Ridley.log.debug(env)
98
+
99
+ env[:body] = self.class.parse(env[:body])
100
+ else
101
+ Ridley.log.debug("Chef Response was not JSON")
102
+ Ridley.log.debug(env)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,305 @@
1
+ module Ridley
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ module Resource
4
+ extend ActiveSupport::Concern
5
+ include ActiveModel::AttributeMethods
6
+ include ActiveModel::Validations
7
+ include ActiveModel::Serializers::JSON
8
+
9
+ included do
10
+ attribute_method_suffix('=')
11
+ end
12
+
13
+ module ClassMethods
14
+ # @return [String, nil]
15
+ def chef_id
16
+ @chef_id
17
+ end
18
+
19
+ # @param [String, Symbol] identifier
20
+ #
21
+ # @return [String]
22
+ def set_chef_id(identifier)
23
+ @chef_id = identifier.to_sym
24
+ end
25
+
26
+ # @return [String]
27
+ def resource_path
28
+ @resource_path ||= self.chef_type.pluralize
29
+ end
30
+
31
+ # @param [String] path
32
+ #
33
+ # @return [String]
34
+ def set_resource_path(path)
35
+ @resource_path = path
36
+ end
37
+
38
+ # @return [String]
39
+ def chef_type
40
+ @chef_type ||= self.class.name.underscore
41
+ end
42
+
43
+ # @param [String, Symbol] type
44
+ #
45
+ # @return [String]
46
+ def set_chef_type(type)
47
+ @chef_type = type.to_s
48
+ attribute(:chef_type, default: type)
49
+ end
50
+
51
+ # @return [String, nil]
52
+ def chef_json_class
53
+ @chef_json_class
54
+ end
55
+
56
+ # @param [String, Symbol] klass
57
+ #
58
+ # @return [String]
59
+ def set_chef_json_class(klass)
60
+ @chef_json_class = klass
61
+ attribute(:json_class, default: klass)
62
+ end
63
+
64
+ # @return [Set]
65
+ def attributes
66
+ @attributes ||= Set.new
67
+ end
68
+
69
+ # @return [Hash]
70
+ def attribute_defaults
71
+ @attribute_defaults ||= Hash.new
72
+ end
73
+
74
+ # @param [String, Symbol] name
75
+ # @option options [Object] :default
76
+ # defines the default value for the attribute
77
+ #
78
+ # @return [Set]
79
+ def attribute(name, options = {})
80
+ if options.has_key?(:default)
81
+ default_for_attribute(name, options[:default])
82
+ end
83
+ define_attribute_method(name)
84
+ attributes << name.to_sym
85
+ end
86
+
87
+ # @param [Ridley::Connection] connection
88
+ #
89
+ # @return [Array<Object>]
90
+ def all(connection)
91
+ connection.get(self.resource_path).body.collect do |identity, location|
92
+ new(connection, self.chef_id => identity)
93
+ end
94
+ end
95
+
96
+ # @param [Ridley::Connection] connection
97
+ # @param [String, #chef_id] object
98
+ #
99
+ # @return [nil, Object]
100
+ def find(connection, object)
101
+ find!(connection, object)
102
+ rescue Errors::HTTPNotFound
103
+ nil
104
+ end
105
+
106
+ # @param [Ridley::Connection] connection
107
+ # @param [String, #chef_id] object
108
+ #
109
+ # @raise [Errors::HTTPNotFound]
110
+ # if a resource with the given chef_id is not found
111
+ #
112
+ # @return [Object]
113
+ def find!(connection, object)
114
+ chef_id = object.respond_to?(:chef_id) ? object.chef_id : object
115
+ new(connection, connection.get("#{self.resource_path}/#{chef_id}").body)
116
+ end
117
+
118
+ # @param [Ridley::Connection] connection
119
+ # @param [#to_hash] object
120
+ #
121
+ # @return [Object]
122
+ def create(connection, object)
123
+ resource = new(connection, object.to_hash)
124
+ new_attributes = connection.post(self.resource_path, resource.to_json).body
125
+ resource.attributes = resource.attributes.merge(new_attributes)
126
+ resource
127
+ end
128
+
129
+ # @param [Ridley::Connection] connection
130
+ # @param [String, #chef_id] object
131
+ #
132
+ # @return [Object]
133
+ def delete(connection, object)
134
+ chef_id = object.respond_to?(:chef_id) ? object.chef_id : object
135
+ new(connection, connection.delete("#{self.resource_path}/#{chef_id}").body)
136
+ end
137
+
138
+ # @param [Ridley::Connection] connection
139
+ #
140
+ # @return [Array<Object>]
141
+ def delete_all(connection)
142
+ mutex = Mutex.new
143
+ deleted = []
144
+ resources = all(connection)
145
+
146
+ connection.thread_count.times.collect do
147
+ Thread.new(connection, resources, deleted) do |connection, resources, deleted|
148
+ while resource = mutex.synchronize { resources.pop }
149
+ result = delete(connection, resource)
150
+ mutex.synchronize { deleted << result }
151
+ end
152
+ end
153
+ end.each(&:join)
154
+
155
+ deleted
156
+ end
157
+
158
+ # @param [Ridley::Connection] connection
159
+ # @param [#to_hash] object
160
+ #
161
+ # @return [Object]
162
+ def update(connection, object)
163
+ resource = new(connection, object.to_hash)
164
+ new(connection, connection.put("#{self.resource_path}/#{resource.chef_id}", resource.to_json).body)
165
+ end
166
+
167
+ private
168
+
169
+ def default_for_attribute(name, value)
170
+ attribute_defaults[name.to_sym] = value
171
+ end
172
+ end
173
+
174
+ # @param [Ridley::Connection] connection
175
+ # @param [Hash] attributes
176
+ def initialize(connection, attributes = {})
177
+ @connection = connection
178
+ self.attributes = self.class.attribute_defaults.merge(attributes)
179
+ end
180
+
181
+ # @param [String, Symbol] key
182
+ #
183
+ # @return [Object]
184
+ def attribute(key)
185
+ if instance_variable_defined?("@#{key}")
186
+ instance_variable_get("@#{key}")
187
+ else
188
+ self.class.attribute_defaults[key]
189
+ end
190
+ end
191
+ alias_method :[], :attribute
192
+
193
+ # @param [String, Symbol] key
194
+ # @param [Object] value
195
+ #
196
+ # @return [Object]
197
+ def attribute=(key, value)
198
+ instance_variable_set("@#{key}", value)
199
+ end
200
+ alias_method :[]=, :attribute=
201
+
202
+ # @param [String, Symbol] key
203
+ #
204
+ # @return [Boolean]
205
+ def attribute?(key)
206
+ attribute(key).present?
207
+ end
208
+
209
+ # @return [Hash]
210
+ def attributes
211
+ {}.tap do |attrs|
212
+ self.class.attributes.each do |attr|
213
+ attrs[attr] = attribute(attr)
214
+ end
215
+ end
216
+ end
217
+
218
+ # @param [#to_hash] new_attributes
219
+ #
220
+ # @return [Hash]
221
+ def attributes=(new_attributes)
222
+ new_attributes.to_hash.symbolize_keys!
223
+
224
+ self.class.attributes.each do |attr_name|
225
+ send(:attribute=, attr_name, new_attributes[attr_name.to_sym])
226
+ end
227
+ end
228
+
229
+ # Creates a resource on the target remote or updates one if the resource
230
+ # already exists.
231
+ #
232
+ # @raise [Errors::InvalidResource]
233
+ # if the resource does not pass validations
234
+ #
235
+ # @return [Boolean]
236
+ # true if successful and false for failure
237
+ def save
238
+ raise Errors::InvalidResource.new(self.errors) unless valid?
239
+
240
+ self.attributes = self.class.create(connection, self).attributes
241
+ true
242
+ rescue Errors::HTTPConflict
243
+ self.attributes = self.class.update(connection, self).attributes
244
+ true
245
+ end
246
+
247
+ # @return [String]
248
+ def chef_id
249
+ attribute(self.class.chef_id)
250
+ end
251
+
252
+ # @param [String] json
253
+ # @option options [Boolean] :symbolize_keys
254
+ # @option options [Class, Symbol, String] :adapter
255
+ #
256
+ # @return [Object]
257
+ def from_json(json, options = {})
258
+ self.attributes = MultiJson.load(json, options)
259
+ self
260
+ end
261
+
262
+ # @param [#to_hash] hash
263
+ #
264
+ # @return [Object]
265
+ def from_hash(hash)
266
+ self.attributes = hash.to_hash
267
+ self
268
+ end
269
+
270
+ # @option options [Boolean] :symbolize_keys
271
+ # @option options [Class, Symbol, String] :adapter
272
+ #
273
+ # @return [String]
274
+ def to_json(options = {})
275
+ MultiJson.dump(self.attributes, options)
276
+ end
277
+ alias_method :as_json, :to_json
278
+
279
+ def to_hash
280
+ self.attributes
281
+ end
282
+
283
+ def to_s
284
+ self.attributes
285
+ end
286
+
287
+ # @param [Object] other
288
+ #
289
+ # @return [Boolean]
290
+ def ==(other)
291
+ self.attributes == other.attributes
292
+ end
293
+
294
+ # @param [Object] other
295
+ #
296
+ # @return [Boolean]
297
+ def eql?(other)
298
+ other.is_a?(self.class) && send(:==, other)
299
+ end
300
+
301
+ private
302
+
303
+ attr_reader :connection
304
+ end
305
+ end
@@ -0,0 +1,75 @@
1
+ module Ridley
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ class Client
4
+ include Ridley::Resource
5
+
6
+ class << self
7
+ # Retrieves a client from the remote connection matching the given chef_id
8
+ # and regenerates it's private key. An instance of the updated object will
9
+ # be returned and have a value set for the 'private_key' accessor.
10
+ #
11
+ # @param [Ridley::Connection] connection
12
+ # @param [String, #chef_id] client
13
+ #
14
+ # @raise [Errors::HTTPNotFound]
15
+ # if a client with the given chef_id is not found
16
+ # @raise [Errors::HTTPError]
17
+ #
18
+ # @return [Ridley::Client]
19
+ def regenerate_key(connection, client)
20
+ obj = find!(connection, client)
21
+ obj.regenerate_key
22
+ obj
23
+ end
24
+ end
25
+
26
+ set_chef_id "name"
27
+ set_chef_type "client"
28
+ set_chef_json_class "Chef::ApiClient"
29
+ set_resource_path "clients"
30
+
31
+ attribute :name
32
+ validates_presence_of :name
33
+
34
+ attribute :admin, default: false
35
+ validates_inclusion_of :admin, in: [ true, false ]
36
+
37
+ attribute :validator, default: false
38
+ validates_inclusion_of :validator, in: [ true, false ]
39
+
40
+ attribute :certificate
41
+ attribute :public_key
42
+ attribute :private_key
43
+ attribute :orgname
44
+
45
+ def attributes
46
+ # @todo JW: reflect on the connection type to determine if we need to strip the
47
+ # json_class attribute. Only OHC/OPC needs this stripped.
48
+ super.except(:json_class)
49
+ end
50
+
51
+ # Regenerates the private key of the instantiated client object. The new
52
+ # private key will be set to the value of the 'private_key' accessor
53
+ # of the instantiated client object.
54
+ #
55
+ # @return [Boolean]
56
+ # true for success and false for failure
57
+ def regenerate_key
58
+ self.private_key = true
59
+ self.save
60
+ end
61
+ end
62
+
63
+ module DSL
64
+ # Coerces instance functions into class functions on Ridley::Client. This coercion
65
+ # sends an instance of the including class along to the class function.
66
+ #
67
+ # @see Ridley::Context
68
+ #
69
+ # @return [Ridley::Context]
70
+ # a context object to delegate instance functions to class functions on Ridley::Client
71
+ def client
72
+ Context.new(Ridley::Client, self)
73
+ end
74
+ end
75
+ end