ridley 0.7.0.beta → 0.7.0.rc1
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/README.md +51 -54
- data/lib/ridley.rb +7 -13
- data/lib/ridley/client.rb +251 -0
- data/lib/ridley/connection.rb +32 -188
- data/lib/ridley/middleware/chef_auth.rb +4 -1
- data/lib/ridley/resource.rb +36 -42
- data/lib/ridley/resources.rb +3 -0
- data/lib/ridley/resources/{client.rb → client_resource.rb} +7 -20
- data/lib/ridley/resources/cookbook_resource.rb +121 -0
- data/lib/ridley/resources/{data_bag_item.rb → data_bag_item_resource.rb} +52 -63
- data/lib/ridley/resources/data_bag_resource.rb +74 -0
- data/lib/ridley/resources/encrypted_data_bag_item_resource.rb +55 -0
- data/lib/ridley/resources/{environment.rb → environment_resource.rb} +8 -21
- data/lib/ridley/resources/{node.rb → node_resource.rb} +24 -37
- data/lib/ridley/resources/{role.rb → role_resource.rb} +1 -14
- data/lib/ridley/resources/sandbox_resource.rb +86 -0
- data/lib/ridley/resources/search.rb +24 -55
- data/lib/ridley/sandbox_uploader.rb +118 -0
- data/lib/ridley/ssh.rb +2 -2
- data/lib/ridley/ssh/worker.rb +2 -1
- data/lib/ridley/version.rb +1 -1
- data/ridley.gemspec +1 -1
- data/spec/acceptance/bootstrapping_spec.rb +1 -1
- data/spec/acceptance/client_resource_spec.rb +18 -20
- data/spec/acceptance/cookbook_resource_spec.rb +4 -22
- data/spec/acceptance/data_bag_item_resource_spec.rb +5 -7
- data/spec/acceptance/data_bag_resource_spec.rb +4 -6
- data/spec/acceptance/environment_resource_spec.rb +14 -16
- data/spec/acceptance/node_resource_spec.rb +12 -14
- data/spec/acceptance/role_resource_spec.rb +13 -15
- data/spec/acceptance/sandbox_resource_spec.rb +7 -9
- data/spec/acceptance/search_resource_spec.rb +6 -8
- data/spec/support/shared_examples/ridley_resource.rb +23 -22
- data/spec/unit/ridley/client_spec.rb +153 -0
- data/spec/unit/ridley/connection_spec.rb +8 -221
- data/spec/unit/ridley/resources/{client_spec.rb → client_resource_spec.rb} +4 -4
- data/spec/unit/ridley/resources/cookbook_resource_spec.rb +5 -0
- data/spec/unit/ridley/resources/{data_bag_item_spec.rb → data_bag_item_resource_spec.rb} +2 -2
- data/spec/unit/ridley/resources/{data_bag_spec.rb → data_bag_resource_spec.rb} +3 -3
- data/spec/unit/ridley/resources/{environment_spec.rb → environment_resource_spec.rb} +4 -4
- data/spec/unit/ridley/resources/{node_spec.rb → node_resource_spec.rb} +4 -4
- data/spec/unit/ridley/resources/{role_spec.rb → role_resource_spec.rb} +3 -3
- data/spec/unit/ridley/resources/sandbox_resource_spec.rb +172 -0
- data/spec/unit/ridley/resources/search_spec.rb +34 -30
- data/spec/unit/ridley/sandbox_uploader_spec.rb +99 -0
- data/spec/unit/ridley/ssh_spec.rb +2 -2
- data/spec/unit/ridley_spec.rb +4 -12
- metadata +36 -28
- data/lib/ridley/dsl.rb +0 -58
- data/lib/ridley/resources/cookbook.rb +0 -51
- data/lib/ridley/resources/data_bag.rb +0 -81
- data/lib/ridley/resources/encrypted_data_bag_item.rb +0 -54
- data/lib/ridley/resources/sandbox.rb +0 -154
- data/spec/unit/ridley/resources/cookbook_spec.rb +0 -5
data/lib/ridley/connection.rb
CHANGED
@@ -1,116 +1,24 @@
|
|
1
1
|
module Ridley
|
2
2
|
# @author Jamie Winsor <jamie@vialstudios.com>
|
3
|
-
class Connection
|
4
|
-
class << self
|
5
|
-
def sync(options, &block)
|
6
|
-
conn = new(options)
|
7
|
-
conn.sync(&block)
|
8
|
-
ensure
|
9
|
-
conn.terminate if conn && conn.alive?
|
10
|
-
end
|
11
|
-
|
12
|
-
# @raise [ArgumentError]
|
13
|
-
#
|
14
|
-
# @return [Boolean]
|
15
|
-
def validate_options(options)
|
16
|
-
missing = (REQUIRED_OPTIONS - options.keys)
|
17
|
-
|
18
|
-
unless missing.empty?
|
19
|
-
missing.collect! { |opt| "'#{opt}'" }
|
20
|
-
raise ArgumentError, "Missing required option(s): #{missing.join(', ')}"
|
21
|
-
end
|
22
|
-
|
23
|
-
missing_values = options.slice(*REQUIRED_OPTIONS).select { |key, value| !value.present? }
|
24
|
-
unless missing_values.empty?
|
25
|
-
values = missing_values.keys.collect { |opt| "'#{opt}'" }
|
26
|
-
raise ArgumentError, "Missing value for required option(s): '#{values.join(', ')}'"
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
# A hash of default options to be used in the Connection initializer
|
31
|
-
#
|
32
|
-
# @return [Hash]
|
33
|
-
def default_options
|
34
|
-
{
|
35
|
-
thread_count: DEFAULT_THREAD_COUNT,
|
36
|
-
ssh: Hash.new
|
37
|
-
}
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
extend Forwardable
|
42
|
-
|
3
|
+
class Connection < Faraday::Connection
|
43
4
|
include Celluloid
|
44
|
-
include Ridley::DSL
|
45
|
-
include Ridley::Logging
|
46
|
-
|
47
|
-
attr_reader :organization
|
48
5
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
6
|
+
VALID_OPTIONS = [
|
7
|
+
:params,
|
8
|
+
:headers,
|
9
|
+
:request,
|
10
|
+
:ssl,
|
11
|
+
:proxy
|
12
|
+
]
|
54
13
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
def_delegator :conn, :build_url
|
59
|
-
def_delegator :conn, :scheme
|
60
|
-
def_delegator :conn, :host
|
61
|
-
def_delegator :conn, :port
|
62
|
-
def_delegator :conn, :path_prefix
|
63
|
-
|
64
|
-
def_delegator :conn, :url_prefix=
|
65
|
-
def_delegator :conn, :url_prefix
|
66
|
-
|
67
|
-
def_delegator :conn, :get
|
68
|
-
def_delegator :conn, :put
|
69
|
-
def_delegator :conn, :post
|
70
|
-
def_delegator :conn, :delete
|
71
|
-
def_delegator :conn, :head
|
72
|
-
|
73
|
-
def_delegator :conn, :in_parallel
|
74
|
-
|
75
|
-
OPTIONS = [
|
76
|
-
:server_url,
|
77
|
-
:client_name,
|
78
|
-
:client_key,
|
79
|
-
:organization,
|
80
|
-
:validator_client,
|
81
|
-
:validator_path,
|
82
|
-
:encrypted_data_bag_secret_path,
|
83
|
-
:thread_count,
|
84
|
-
:ssl
|
85
|
-
].freeze
|
86
|
-
|
87
|
-
REQUIRED_OPTIONS = [
|
88
|
-
:server_url,
|
89
|
-
:client_name,
|
90
|
-
:client_key
|
91
|
-
].freeze
|
92
|
-
|
93
|
-
DEFAULT_THREAD_COUNT = 8
|
14
|
+
attr_reader :organization
|
15
|
+
attr_reader :client_key
|
16
|
+
attr_reader :client_name
|
94
17
|
|
95
|
-
# @
|
96
|
-
#
|
97
|
-
# @
|
98
|
-
#
|
99
|
-
# @option options [String] :client_key
|
100
|
-
# filepath to the client's private key used to authenticate with the Chef API
|
101
|
-
# @option options [String] :organization
|
102
|
-
# the Organization to connect to. This is only used if you are connecting to
|
103
|
-
# private Chef or hosted Chef
|
104
|
-
# @option options [String] :validator_client (nil)
|
105
|
-
# @option options [String] :validator_path (nil)
|
106
|
-
# @option options [String] :encrypted_data_bag_secret_path (nil)
|
107
|
-
# @option options [Integer] :thread_count (DEFAULT_THREAD_COUNT)
|
108
|
-
# @option options [Hash] :ssh (Hash.new)
|
109
|
-
# * :user (String) a shell user that will login to each node and perform the bootstrap command on (required)
|
110
|
-
# * :password (String) the password for the shell user that will perform the bootstrap
|
111
|
-
# * :keys (Array, String) an array of keys (or a single key) to authenticate the ssh user with instead of a password
|
112
|
-
# * :timeout (Float) [5.0] timeout value for SSH bootstrap
|
113
|
-
# * :sudo (Boolean) [true] bootstrap with sudo
|
18
|
+
# @param [String] :server_url
|
19
|
+
# @param [String] :client_name
|
20
|
+
# @param [String] :client_key
|
21
|
+
#
|
114
22
|
# @option options [Hash] :params
|
115
23
|
# URI query unencoded key/value pairs
|
116
24
|
# @option options [Hash] :headers
|
@@ -121,63 +29,37 @@ module Ridley
|
|
121
29
|
# * :verify (Boolean) [true] set to false to disable SSL verification
|
122
30
|
# @option options [URI, String, Hash] :proxy
|
123
31
|
# URI, String, or Hash of HTTP proxy options
|
124
|
-
def initialize(options = {})
|
125
|
-
|
126
|
-
|
127
|
-
end
|
128
|
-
|
129
|
-
# Configure this instance of Ridley::Connection
|
130
|
-
#
|
131
|
-
# @param [Hash] options
|
132
|
-
def configure(options)
|
133
|
-
options = self.class.default_options.merge(options)
|
134
|
-
self.class.validate_options(options)
|
32
|
+
def initialize(server_url, client_name, client_key, options = {})
|
33
|
+
@client_name = client_name
|
34
|
+
@client_key = client_key
|
135
35
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
@validator_client = options[:validator_client]
|
142
|
-
@validator_path = options[:validator_path]
|
143
|
-
@encrypted_data_bag_secret_path = options[:encrypted_data_bag_secret_path]
|
36
|
+
options = options.reverse_merge(
|
37
|
+
builder: Faraday::Builder.new { |b|
|
38
|
+
b.request :chef_auth, client_name, client_key
|
39
|
+
b.response :chef_response
|
40
|
+
b.response :json
|
144
41
|
|
145
|
-
|
146
|
-
|
147
|
-
|
42
|
+
b.adapter :net_http_persistent
|
43
|
+
}
|
44
|
+
)
|
148
45
|
|
149
|
-
|
150
|
-
uri_hash = Addressable::URI.parse(options[:server_url]).to_hash.slice(:scheme, :host, :port)
|
46
|
+
uri_hash = Addressable::URI.parse(server_url).to_hash.slice(:scheme, :host, :port)
|
151
47
|
|
152
48
|
unless uri_hash[:port]
|
153
49
|
uri_hash[:port] = (uri_hash[:scheme] == "https" ? 443 : 80)
|
154
50
|
end
|
155
51
|
|
156
|
-
if org_match =
|
157
|
-
@organization
|
52
|
+
if org_match = server_url.match(/.*\/organizations\/(.*)/)
|
53
|
+
@organization = org_match[1]
|
158
54
|
end
|
159
55
|
|
160
|
-
unless organization.nil?
|
161
|
-
uri_hash[:path] = "/organizations/#{organization}"
|
56
|
+
unless @organization.nil?
|
57
|
+
uri_hash[:path] = "/organizations/#{@organization}"
|
162
58
|
end
|
163
59
|
|
164
60
|
server_uri = Addressable::URI.new(uri_hash)
|
165
61
|
|
166
|
-
|
167
|
-
c.request :chef_auth, client_name, client_key
|
168
|
-
c.response :chef_response
|
169
|
-
c.response :json
|
170
|
-
|
171
|
-
c.adapter :net_http_persistent
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
def sync(&block)
|
176
|
-
unless block
|
177
|
-
raise Errors::InternalError, "A block must be given to synchronously process requests."
|
178
|
-
end
|
179
|
-
|
180
|
-
evaluate(&block)
|
62
|
+
super(server_uri, options)
|
181
63
|
end
|
182
64
|
|
183
65
|
# @return [Symbol]
|
@@ -198,43 +80,5 @@ module Ridley
|
|
198
80
|
def server_url
|
199
81
|
self.url_prefix.to_s
|
200
82
|
end
|
201
|
-
|
202
|
-
# The encrypted data bag secret for this connection.
|
203
|
-
#
|
204
|
-
# @raise [Ridley::Errors::EncryptedDataBagSecretNotFound]
|
205
|
-
#
|
206
|
-
# @return [String, nil]
|
207
|
-
def encrypted_data_bag_secret
|
208
|
-
return nil if encrypted_data_bag_secret_path.nil?
|
209
|
-
|
210
|
-
IO.read(encrypted_data_bag_secret_path).chomp
|
211
|
-
rescue Errno::ENOENT => e
|
212
|
-
raise Errors::EncryptedDataBagSecretNotFound, "Encrypted data bag secret provided but not found at '#{encrypted_data_bag_secret_path}'"
|
213
|
-
end
|
214
|
-
|
215
|
-
def finalize
|
216
|
-
log.info { "Ridley stopping..." }
|
217
|
-
end
|
218
|
-
|
219
|
-
private
|
220
|
-
|
221
|
-
attr_reader :conn
|
222
|
-
|
223
|
-
def evaluate(&block)
|
224
|
-
@self_before_instance_eval = eval("self", block.binding)
|
225
|
-
instance_eval(&block)
|
226
|
-
end
|
227
|
-
|
228
|
-
def method_missing(method, *args, &block)
|
229
|
-
if block_given?
|
230
|
-
@self_before_instance_eval ||= eval("self", block.binding)
|
231
|
-
end
|
232
|
-
|
233
|
-
if @self_before_instance_eval.nil?
|
234
|
-
super
|
235
|
-
end
|
236
|
-
|
237
|
-
@self_before_instance_eval.send(method, *args, &block)
|
238
|
-
end
|
239
83
|
end
|
240
84
|
end
|
@@ -4,6 +4,8 @@ module Ridley
|
|
4
4
|
module Middleware
|
5
5
|
# @author Jamie Winsor <jamie@vialstudios.com>
|
6
6
|
class ChefAuth < Faraday::Middleware
|
7
|
+
include Ridley::Logging
|
8
|
+
|
7
9
|
attr_reader :client_name
|
8
10
|
attr_reader :client_key
|
9
11
|
|
@@ -26,7 +28,8 @@ module Ridley
|
|
26
28
|
env[:request_headers] = default_headers.merge(env[:request_headers]).merge(authentication_headers)
|
27
29
|
env[:request_headers] = env[:request_headers].merge('Content-Length' => env[:body].bytesize.to_s) if env[:body]
|
28
30
|
|
29
|
-
|
31
|
+
log.debug { "Performing Authenticated Chef Request: "}
|
32
|
+
log.debug { env }
|
30
33
|
|
31
34
|
@app.call(env)
|
32
35
|
end
|
data/lib/ridley/resource.rb
CHANGED
@@ -52,94 +52,88 @@ module Ridley
|
|
52
52
|
attribute(:json_class, default: klass)
|
53
53
|
end
|
54
54
|
|
55
|
-
# @param [Ridley::
|
55
|
+
# @param [Ridley::Client] client
|
56
56
|
#
|
57
57
|
# @return [Array<Object>]
|
58
|
-
def all(
|
59
|
-
connection.get(self.resource_path).body.collect do |identity, location|
|
60
|
-
new(
|
58
|
+
def all(client)
|
59
|
+
client.connection.get(self.resource_path).body.collect do |identity, location|
|
60
|
+
new(client, self.chef_id => identity)
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
-
# @param [Ridley::
|
64
|
+
# @param [Ridley::Client] client
|
65
65
|
# @param [String, #chef_id] object
|
66
66
|
#
|
67
67
|
# @return [nil, Object]
|
68
|
-
def find(
|
69
|
-
find!(
|
68
|
+
def find(client, object)
|
69
|
+
find!(client, object)
|
70
70
|
rescue Errors::HTTPNotFound
|
71
71
|
nil
|
72
72
|
end
|
73
73
|
|
74
|
-
# @param [Ridley::
|
74
|
+
# @param [Ridley::Client] client
|
75
75
|
# @param [String, #chef_id] object
|
76
76
|
#
|
77
77
|
# @raise [Errors::HTTPNotFound]
|
78
78
|
# if a resource with the given chef_id is not found
|
79
79
|
#
|
80
80
|
# @return [Object]
|
81
|
-
def find!(
|
81
|
+
def find!(client, object)
|
82
82
|
chef_id = object.respond_to?(:chef_id) ? object.chef_id : object
|
83
|
-
new(
|
83
|
+
new(client, client.connection.get("#{self.resource_path}/#{chef_id}").body)
|
84
84
|
end
|
85
85
|
|
86
|
-
# @param [Ridley::
|
86
|
+
# @param [Ridley::Client] client
|
87
87
|
# @param [#to_hash] object
|
88
88
|
#
|
89
89
|
# @return [Object]
|
90
|
-
def create(
|
91
|
-
resource = new(
|
92
|
-
new_attributes = connection.post(self.resource_path, resource.to_json).body
|
90
|
+
def create(client, object)
|
91
|
+
resource = new(client, object.to_hash)
|
92
|
+
new_attributes = client.connection.post(self.resource_path, resource.to_json).body
|
93
93
|
resource.attributes = resource.attributes.deep_merge(new_attributes)
|
94
94
|
resource
|
95
95
|
end
|
96
96
|
|
97
|
-
# @param [Ridley::
|
97
|
+
# @param [Ridley::Client] client
|
98
98
|
# @param [String, #chef_id] object
|
99
99
|
#
|
100
100
|
# @return [Object]
|
101
|
-
def delete(
|
101
|
+
def delete(client, object)
|
102
102
|
chef_id = object.respond_to?(:chef_id) ? object.chef_id : object
|
103
|
-
new(
|
103
|
+
new(client, client.connection.delete("#{self.resource_path}/#{chef_id}").body)
|
104
104
|
end
|
105
105
|
|
106
|
-
# @param [Ridley::
|
106
|
+
# @param [Ridley::Client] client
|
107
107
|
#
|
108
108
|
# @return [Array<Object>]
|
109
|
-
def delete_all(
|
109
|
+
def delete_all(client)
|
110
110
|
mutex = Mutex.new
|
111
111
|
deleted = []
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
mutex.synchronize { deleted << result }
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end.each(&:join)
|
122
|
-
|
123
|
-
deleted
|
112
|
+
|
113
|
+
all(client).collect do |resource|
|
114
|
+
Celluloid::Future.new {
|
115
|
+
delete(client, resource)
|
116
|
+
}
|
117
|
+
end.map(&:value)
|
124
118
|
end
|
125
119
|
|
126
|
-
# @param [Ridley::
|
120
|
+
# @param [Ridley::Client] client
|
127
121
|
# @param [#to_hash] object
|
128
122
|
#
|
129
123
|
# @return [Object]
|
130
|
-
def update(
|
131
|
-
resource = new(
|
132
|
-
new(
|
124
|
+
def update(client, object)
|
125
|
+
resource = new(client, object.to_hash)
|
126
|
+
new(client, client.connection.put("#{self.resource_path}/#{resource.chef_id}", resource.to_json).body)
|
133
127
|
end
|
134
128
|
end
|
135
129
|
|
136
130
|
include Chozo::VariaModel
|
137
131
|
include Comparable
|
138
132
|
|
139
|
-
# @param [Ridley::
|
133
|
+
# @param [Ridley::Client] client
|
140
134
|
# @param [Hash] new_attrs
|
141
|
-
def initialize(
|
142
|
-
@
|
135
|
+
def initialize(client, new_attrs = {})
|
136
|
+
@client = client
|
143
137
|
mass_assign(new_attrs)
|
144
138
|
end
|
145
139
|
|
@@ -153,7 +147,7 @@ module Ridley
|
|
153
147
|
def save
|
154
148
|
raise Errors::InvalidResource.new(self.errors) unless valid?
|
155
149
|
|
156
|
-
mass_assign(self.class.create(
|
150
|
+
mass_assign(self.class.create(client, self).attributes)
|
157
151
|
true
|
158
152
|
rescue Errors::HTTPConflict
|
159
153
|
self.update
|
@@ -170,7 +164,7 @@ module Ridley
|
|
170
164
|
def update
|
171
165
|
raise Errors::InvalidResource.new(self.errors) unless valid?
|
172
166
|
|
173
|
-
mass_assign(self.class.update(
|
167
|
+
mass_assign(self.class.update(client, self).attributes)
|
174
168
|
true
|
175
169
|
end
|
176
170
|
|
@@ -178,7 +172,7 @@ module Ridley
|
|
178
172
|
#
|
179
173
|
# @return [Object]
|
180
174
|
def reload
|
181
|
-
mass_assign(self.class.find(
|
175
|
+
mass_assign(self.class.find(client, self).attributes)
|
182
176
|
self
|
183
177
|
end
|
184
178
|
|
@@ -215,6 +209,6 @@ module Ridley
|
|
215
209
|
|
216
210
|
private
|
217
211
|
|
218
|
-
attr_reader :
|
212
|
+
attr_reader :client
|
219
213
|
end
|
220
214
|
end
|
@@ -1,21 +1,21 @@
|
|
1
1
|
module Ridley
|
2
2
|
# @author Jamie Winsor <jamie@vialstudios.com>
|
3
|
-
class
|
3
|
+
class ClientResource < Ridley::Resource
|
4
4
|
class << self
|
5
5
|
# Retrieves a client from the remote connection matching the given chef_id
|
6
6
|
# and regenerates it's private key. An instance of the updated object will
|
7
7
|
# be returned and have a value set for the 'private_key' accessor.
|
8
8
|
#
|
9
|
-
# @param [Ridley::
|
10
|
-
# @param [String, #chef_id]
|
9
|
+
# @param [Ridley::Client] client
|
10
|
+
# @param [String, #chef_id] chef_client
|
11
11
|
#
|
12
12
|
# @raise [Errors::HTTPNotFound]
|
13
13
|
# if a client with the given chef_id is not found
|
14
14
|
# @raise [Errors::HTTPError]
|
15
15
|
#
|
16
|
-
# @return [Ridley::
|
17
|
-
def regenerate_key(
|
18
|
-
obj = find!(
|
16
|
+
# @return [Ridley::ClientResource]
|
17
|
+
def regenerate_key(client, chef_client)
|
18
|
+
obj = find!(client, chef_client)
|
19
19
|
obj.regenerate_key
|
20
20
|
obj
|
21
21
|
end
|
@@ -66,24 +66,11 @@ module Ridley
|
|
66
66
|
# Override to_json to reflect to massage the returned attributes based on the type
|
67
67
|
# of connection. Only OHC/OPC requires the json_class attribute is not present.
|
68
68
|
def to_json
|
69
|
-
if connection.hosted?
|
69
|
+
if client.connection.hosted?
|
70
70
|
attributes.except(:json_class).to_json
|
71
71
|
else
|
72
72
|
super
|
73
73
|
end
|
74
74
|
end
|
75
75
|
end
|
76
|
-
|
77
|
-
module DSL
|
78
|
-
# Coerces instance functions into class functions on Ridley::Client. This coercion
|
79
|
-
# sends an instance of the including class along to the class function.
|
80
|
-
#
|
81
|
-
# @see Ridley::ChainLink
|
82
|
-
#
|
83
|
-
# @return [Ridley::ChainLink]
|
84
|
-
# a context object to delegate instance functions to class functions on Ridley::Client
|
85
|
-
def client
|
86
|
-
ChainLink.new(self, Ridley::Client)
|
87
|
-
end
|
88
|
-
end
|
89
76
|
end
|