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.
- data/.gitignore +17 -0
- data/.travis.yml +5 -0
- data/Gemfile +3 -0
- data/Guardfile +20 -0
- data/LICENSE +201 -0
- data/README.md +273 -0
- data/Thorfile +48 -0
- data/lib/ridley.rb +48 -0
- data/lib/ridley/connection.rb +131 -0
- data/lib/ridley/context.rb +25 -0
- data/lib/ridley/dsl.rb +58 -0
- data/lib/ridley/errors.rb +82 -0
- data/lib/ridley/log.rb +10 -0
- data/lib/ridley/middleware.rb +19 -0
- data/lib/ridley/middleware/chef_auth.rb +45 -0
- data/lib/ridley/middleware/chef_response.rb +28 -0
- data/lib/ridley/middleware/parse_json.rb +107 -0
- data/lib/ridley/resource.rb +305 -0
- data/lib/ridley/resources/client.rb +75 -0
- data/lib/ridley/resources/cookbook.rb +27 -0
- data/lib/ridley/resources/data_bag.rb +75 -0
- data/lib/ridley/resources/data_bag_item.rb +186 -0
- data/lib/ridley/resources/environment.rb +45 -0
- data/lib/ridley/resources/node.rb +34 -0
- data/lib/ridley/resources/role.rb +33 -0
- data/lib/ridley/version.rb +3 -0
- data/ridley.gemspec +39 -0
- data/spec/acceptance/client_resource_spec.rb +135 -0
- data/spec/acceptance/cookbook_resource_spec.rb +46 -0
- data/spec/acceptance/data_bag_item_resource_spec.rb +171 -0
- data/spec/acceptance/data_bag_resource_spec.rb +51 -0
- data/spec/acceptance/environment_resource_spec.rb +171 -0
- data/spec/acceptance/node_resource_spec.rb +218 -0
- data/spec/acceptance/role_resource_spec.rb +200 -0
- data/spec/fixtures/reset.pem +27 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/each_matcher.rb +12 -0
- data/spec/support/shared_examples/ridley_resource.rb +237 -0
- data/spec/support/spec_helpers.rb +11 -0
- data/spec/unit/ridley/connection_spec.rb +167 -0
- data/spec/unit/ridley/errors_spec.rb +34 -0
- data/spec/unit/ridley/middleware/chef_auth_spec.rb +14 -0
- data/spec/unit/ridley/middleware/chef_response_spec.rb +213 -0
- data/spec/unit/ridley/middleware/parse_json_spec.rb +74 -0
- data/spec/unit/ridley/resource_spec.rb +214 -0
- data/spec/unit/ridley/resources/client_spec.rb +47 -0
- data/spec/unit/ridley/resources/cookbook_spec.rb +5 -0
- data/spec/unit/ridley/resources/data_bag_item_spec.rb +42 -0
- data/spec/unit/ridley/resources/data_bag_spec.rb +15 -0
- data/spec/unit/ridley/resources/environment_spec.rb +73 -0
- data/spec/unit/ridley/resources/node_spec.rb +5 -0
- data/spec/unit/ridley/resources/role_spec.rb +5 -0
- data/spec/unit/ridley_spec.rb +32 -0
- 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
|