ridley 0.0.1

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