consul-ruby-client 0.0.2

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ea81d2569729debbb271b229358775f919a0de0c
4
+ data.tar.gz: 521ad0389c65a8d57ca62202faada9546d467416
5
+ SHA512:
6
+ metadata.gz: 5c86f347a8a0ca26a65a4cf825688544ce14d75d2988a6e38d78c5c4adfdb3f9f9cbfbef0ebe78dab87212220bf13bcf68a43be07f18160aa1253ceac90e7073
7
+ data.tar.gz: 93e0c798a11880043e1545daa87eb84e6210af4f91f5849bd1640c0a3adbc6db127db645943f7a5ed40244bcbb65591367d05196ed177908bae3c2cc6c27546a
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+
16
+ # Ignore all development gems.
17
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in consul-ruby-client.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Michael Hotan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Michael Hotan
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # Consul::Client
2
+
3
+ Thin Ruby Client around Consul REST API
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'consul-ruby-client'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install consul-ruby-client
20
+
21
+ ## Usage
22
+
23
+ To use the client you have to require the root library
24
+
25
+ ```
26
+ # Add the dependencies
27
+ require 'consul/client'
28
+
29
+ # Import that namespace
30
+ include Consul::Client
31
+ ```
32
+
33
+ ### Key Value Store
34
+
35
+ ```
36
+ kvs = KeyValue.new
37
+ kvs.put('cat','dog')
38
+ kvs.get('cat')
39
+
40
+ ```
41
+
42
+ ### Agent
43
+
44
+ ```
45
+ agent = Agent.new
46
+ agent.register(Agent::Service.for_name('my_service'))
47
+ agent.services
48
+ ```
49
+
50
+ ### Catalog
51
+
52
+ TODO
53
+
54
+ ### Sessions
55
+
56
+ TODO
57
+
58
+ ### Status
59
+
60
+ TODO
61
+
62
+ ## TODO
63
+
64
+ * Tests,
65
+ Currently all test were done throught building and installing the ruby client
66
+ and verifying through REPL. That is not the long term solution. We are looking
67
+ at integrating Consul into the rspec test itself.
68
+ However a solid short term win will be completing rspec tests with set fixtures.
69
+
70
+ * Implement more advance locking mechanisms
71
+ * Complete the Agent API self, join, and force-leave
72
+ * ACL API
73
+ * Events
74
+
75
+ ## Contributing
76
+
77
+ 1. Fork it ( https://github.com/[my-github-username]/consul-client/fork )
78
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
79
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
80
+ 4. Push to the branch (`git push origin my-new-feature`)
81
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'consul/client/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'consul-ruby-client'
8
+ spec.version = Consul::Client::VERSION
9
+ spec.authors = ['Michael Hotan']
10
+ spec.email = ['michael.hotan@socrata.com']
11
+ spec.summary = %q{Ruby Client library for communicating with Consul Agents.}
12
+ spec.description = %q{Consul agents expose a HTTP REST API and this library is used to communicate directly with it.}
13
+ spec.homepage = 'https://github.com/mhotan-s/consul-ruby-client'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.7'
22
+ spec.add_development_dependency 'rake', '~> 10.0'
23
+ spec.add_development_dependency 'rspec', '~> 3.2'
24
+ spec.add_development_dependency 'simplecov', '~> 0.9'
25
+ spec.add_development_dependency 'webmock', '~> 1.21'
26
+ spec.add_dependency 'rest-client', '~> 1.6'
27
+ spec.add_dependency 'representable', '~> 2.1'
28
+ spec.add_dependency 'json', '~> 1.8'
29
+ end
@@ -0,0 +1,18 @@
1
+ require_relative '../consul/model/health_check'
2
+ require_relative '../consul/model/key_value'
3
+ require_relative '../consul/model/node'
4
+ require_relative '../consul/model/service'
5
+ require_relative '../consul/model/session'
6
+ require_relative '../consul/client/version'
7
+ require_relative '../consul/client/base'
8
+ require_relative '../consul/client/agent'
9
+ require_relative '../consul/client/catalog'
10
+ require_relative '../consul/client/key_value'
11
+ require_relative '../consul/client/session'
12
+ require_relative '../consul/client/status'
13
+
14
+ module Consul
15
+ module Client
16
+ # Modules added above.
17
+ end
18
+ end
@@ -0,0 +1,322 @@
1
+ require_relative 'base'
2
+ require_relative '../model/service'
3
+ require_relative '../model/health_check'
4
+
5
+ # Consul Agent Client.
6
+ # Represents Ruby endpoint as described here: https://www.consul.io/docs/agent/http/agent.html
7
+ module Consul
8
+ module Client
9
+ class Agent
10
+ include Consul::Client::Base
11
+
12
+ # Public: Returns all the services registered with this Agent.
13
+ #
14
+ # Returns: An array of all the ConsulService(s) registered on this agent.
15
+ def services
16
+ begin
17
+ resp = _get build_agent_url('services')
18
+ rescue
19
+ @logger.warn('Unable to request all the services on this through the HTTP API')
20
+ return nil
21
+ end
22
+ # Consul returns id => ConsulServiceObjects.
23
+ s_hash = JSON.parse(resp)
24
+ s_hash.keys.map { |n| Consul::Model::Service.new.extend(Consul::Model::Service::Representer).from_hash(s_hash[n]) }
25
+ end
26
+
27
+ # Public: Returns the service that has the associated name.
28
+ #
29
+ # Returns: ConsulService if exists, nil if no service by this name exists.
30
+ def service(id)
31
+ ss = services
32
+ ss.keep_if {|s| s.id == id}.first unless ss.nil?
33
+ end
34
+
35
+ def checks
36
+ begin
37
+ resp = _get build_agent_url('checks')
38
+ rescue
39
+ @logger.warn('Unable to request all the checks on this through the HTTP API')
40
+ return nil
41
+ end
42
+ # Consul returns id => ConsulServiceObjects.
43
+ s_hash = JSON.parse(resp)
44
+ s_hash.keys.map { |n| Consul::Model::HealthCheck.new.extend(Consul::Model::HealthCheck::Representer).from_hash(s_hash[n]) }
45
+ end
46
+
47
+ # Public: Returns a check by a given id.
48
+ # Returns: nil if no such check exists, or the check instance with the correct id
49
+ def check(id)
50
+ c = checks
51
+ c.keep_if {|c| c.check_id == id}.first unless c.nil?
52
+ end
53
+
54
+ # Public: Returns a Health Check for a specific service.
55
+ #
56
+ # service_id - The ID of the service.
57
+ def service_check(service_id)
58
+ check("service:#{service_id}")
59
+ end
60
+
61
+ # Public: Registers either a service or a Health check with configured consul agent.
62
+ #
63
+ # entity - Consul::Model::Service or Consul::Model::HealthCheck instance.
64
+ #
65
+ # Example
66
+ # agent = Consul::Client::Agent.new('dc1')
67
+ # # Register a service
68
+ # agent.register(Consul::Client::Agent::Service.for_name('cat'))
69
+ # # Register a HealthCheck
70
+ # agent.register(Consul::Client::HealthCheck.ttl('my_check_name', '15m'))
71
+ # # Register a service with a Consul Health Check
72
+ # agent.register(Consul::Client::Agent::Service.for_name('cat', Consul::Client::Agent::Service.ttl_health_check('15m')))
73
+ #
74
+ # Returns true upon success, false upon failure
75
+ def register(entity)
76
+ raise TypeError unless entity.kind_of? Consul::Model::HealthCheck or entity.kind_of? Consul::Model::Service
77
+ case entity
78
+ when Consul::Model::HealthCheck
79
+ return register_with_backoff(build_check_url('register'), entity.extend(Consul::Model::HealthCheck::Representer), 0, 3)
80
+ else
81
+ entity = entity.extend(Consul::Model::Service::Representer)
82
+ success = register_with_backoff(build_service_url('register'), entity, 0, 3)
83
+ if success
84
+ @logger.info("Successfully registered service #{entity.name}.")
85
+ unless entity.check.nil?
86
+ # Pass the first health check
87
+ c = check("service:#{entity.name}")
88
+ @logger.info("Updating status for health check #{c.check_id} to \"pass\".")
89
+ _get build_check_status_url(c.check_id, 'pass')
90
+ end
91
+ end
92
+ return success
93
+ end
94
+ end
95
+
96
+ # Public: deregisters an existing ConsulHealthCheck or ConsulService
97
+ #
98
+ # entity - ConsulHealthCheck or ConsulService to unregister from this
99
+ #
100
+ # Returns - the HTTP Response
101
+ def deregister(entity)
102
+ unless entity.nil?
103
+ raise TypeError unless entity.kind_of? Consul::Model::HealthCheck or entity.kind_of? Consul::Model::Service
104
+ case entity
105
+ when Consul::Model::HealthCheck
106
+ url = build_check_url('deregister')
107
+ else
108
+ url = build_service_url('deregister')
109
+ end
110
+ _get "#{url}/#{entity.id}"
111
+ end
112
+ end
113
+
114
+ # Public: Pass a health check.
115
+ #
116
+ # check - Consul::Model::HealthCheck to pass. Cannot be nil or wrong type
117
+ #
118
+ def pass(check)
119
+ update_check_status(check, 'pass')
120
+ end
121
+
122
+ # Public: Warn a health check
123
+ #
124
+ # check - Consul::Model::HealthCheck to pass. Cannot be nil or wrong type
125
+ #
126
+ def warn(check)
127
+ update_check_status(check, 'warn')
128
+ end
129
+
130
+ # Public: Fails a health check
131
+ #
132
+ # check - Consul::Model::HealthCheck to pass. Cannot be nil or wrong type
133
+ #
134
+ def fail(check)
135
+ update_check_status(check, 'fail')
136
+ end
137
+
138
+ # Public: Enter maintenance mode.
139
+ #
140
+ # enable - Flag to indicate to enable maintanence mode or not
141
+ # service - Set maintanence for a particular service is set.
142
+ # reason - Optional reason.
143
+ def maintenance(enable, service = nil, reason = nil)
144
+ if service.nil?
145
+ url = build_agent_url('maintenance')
146
+ else
147
+ if service.instance_of?(Consul::Model::Service)
148
+ service = service.id
149
+ end
150
+ raise ArgumentError.new "Unable to create request for #{service}" unless service.respond_to?(:to_str)
151
+ url = build_service_url("maintenance/#{service}")
152
+ end
153
+ params = {:enable => enable}
154
+ params[:reason] = reason unless reason.nil?
155
+ _get url, params
156
+ end
157
+
158
+ module HealthCheck
159
+
160
+ # Public: TTL Check
161
+ #
162
+ # name - The name of the check, Cannot be nil
163
+ # ttl - Time to live time window. IE "15s", Cannot be nil
164
+ # id - ID to associate with this check if 'name' is not desired.
165
+ # notes - Message to place as notes for this check
166
+ #
167
+ # Returns: Consul::Model::HealthCheck instance
168
+ def self.ttl(name, ttl, id = name, notes = nil)
169
+ validate_arg name
170
+ validate_arg ttl
171
+ c = Consul::Model::HealthCheck.new(name: name, ttl: ttl)
172
+ c[:id] = id unless id.nil?
173
+ c[:notes] = notes unless notes.nil?
174
+ c
175
+ end
176
+
177
+ # Public: TTL Check
178
+ #
179
+ # name - The name of the check, Cannot be nil
180
+ # script - The script to run locally
181
+ # interval - The time interval to conduct the check. IE: '10s'
182
+ # id - ID to associate with this check if 'name' is not desired.
183
+ # notes - Message to place as notes for this check.
184
+ #
185
+ # Returns: Consul::Model::HealthCheck instance
186
+ def self.script(name, script, interval, id = name, notes = nil)
187
+ validate_arg name
188
+ validate_arg script
189
+ validate_arg interval
190
+ c = Consul::Model::HealthCheck.new(name: name, script: script, interval: interval)
191
+ c[:id] = id unless id.nil?
192
+ c[:notes] = notes unless notes.nil?
193
+ c
194
+ end
195
+
196
+ # Public: TTL Check
197
+ #
198
+ # name - The name of the check, Cannot be nil
199
+ # http - The HTTP endpoint to hit with periodic GET.
200
+ # interval - The time interval to conduct the check. IE: '10s'
201
+ # id - ID to associate with this check if 'name' is not desired.
202
+ # notes - Message to place as notes for this check
203
+ #
204
+ # Returns: Consul::Model::HealthCheck instance
205
+ def self.http(name, http, interval, id = name, notes = nil)
206
+ validate_arg name
207
+ validate_arg http
208
+ validate_arg interval
209
+ c = Consul::Model::HealthCheck.new(name: name, script: script, interval: interval)
210
+ c[:id] = id unless id.nil?
211
+ c[:notes] = notes unless notes.nil?
212
+ c
213
+ end
214
+
215
+ private
216
+
217
+ def self.validate_arg(arg)
218
+ raise ArgumentError.new "Illegal Argument: #{arg}" if arg.nil? or arg.empty?
219
+ end
220
+
221
+ end
222
+
223
+ # Container Module for simpler way to create a service.
224
+ module Service
225
+
226
+ # Public: Creates a service using a specific name
227
+ #
228
+ # name - Name of the service to create
229
+ # check - The Consul::Model::HealthCheck instance to associate with this Session
230
+ #
231
+ # Returns: Consul::Model::Service instance
232
+ def self.for_name(name, check = nil)
233
+ raise ArgumentError.new "Illegal name: \"#{name}\" for service." if name.nil?
234
+ unless check.nil? or check.is_a?(Consul::Model::HealthCheck)
235
+ raise TypeError.new "Illegal Check type: #{check}. Expecting Consul::Model::HealthCheck"
236
+ end
237
+ if check.nil?
238
+ Consul::Model::Service.new(name: name)
239
+ else # There is a health check to register
240
+ Consul::Model::Service.new(name: name, check: check)
241
+ end
242
+ end
243
+
244
+ # Public: Creates a health check meant to be used when registering a service.
245
+ #
246
+ #
247
+ #
248
+ # Returns: Consul::Model::HealthCheck instance that represents a script
249
+ def self.script_health_check(script, interval)
250
+ Consul::Model::HealthCheck.new(script: script, interval: interval)
251
+ end
252
+
253
+ def self.http_health_check(http, interval)
254
+ Consul::Model::HealthCheck.new(http: http, interval: interval)
255
+ end
256
+
257
+ def self.ttl_health_check(ttl)
258
+ Consul::Model::HealthCheck.new(ttl: ttl)
259
+ end
260
+ end
261
+
262
+ private
263
+
264
+ # Private: Updates the check with the argument status.
265
+ def update_check_status(check, status)
266
+ unless check.instance_of?(Consul::Model::HealthCheck) and check.respond_to?(:to_str)
267
+ check = check(check.to_str)
268
+ end
269
+ return false if check.nil?
270
+ raise ArgumentError.new "Illegal Status #{status}" unless status == 'pass' or status == 'warn' or status == 'fail'
271
+ resp = _get build_check_url("#{status}/#{check.check_id}")
272
+ resp.code == 200
273
+ end
274
+
275
+ # Private: Register a consul entity with the existing agent but attempts and exponentially increasing interval if
276
+ # fails.
277
+ #
278
+ # url - The url to make the register request through.
279
+ # entity - JSON representation of the entity
280
+ # iteration - The current attempt iteration
281
+ # threshold - Number of attempts to try
282
+ #
283
+ def register_with_backoff(url, entity, iteration, threshold)
284
+ # Checking a greater iteration just to be on the safe side.
285
+ unless iteration > threshold or threshold <= 0 or iteration < 0
286
+ sleep((1.0/2.0*(2.0**iteration - 1.0)).ceil) if iteration > 0
287
+ success, _ = _put(url, entity.to_json)
288
+ unless success # Unless we successfully registered.
289
+ if threshold == iteration
290
+ @logger.error("Unable to complete registration after #{threshold + 1} attempts")
291
+ else
292
+ # Attempt to register again using the exponential backoff.
293
+ @logger.warn("Unable to complete registration after #{iteration + 1} attempts, Retrying up to #{threshold+1} attempts")
294
+ register_with_backoff(url, entity, iteration + 1, threshold)
295
+ end
296
+ end
297
+ return true
298
+ end
299
+ false
300
+ end
301
+
302
+ def build_service_url(suffix)
303
+ build_agent_url("service/#{suffix}")
304
+ end
305
+
306
+ def build_check_url(suffix)
307
+ build_agent_url("check/#{suffix}")
308
+ end
309
+
310
+ # status : "pass", "warn", or "fail"
311
+ def build_check_status_url(check_id, status)
312
+ build_check_url("#{status}/#{check_id}")
313
+ end
314
+
315
+ # Returns host:port/v1/agent/suffix
316
+ def build_agent_url(suffix)
317
+ "#{base_versioned_url}/agent/#{suffix}"
318
+ end
319
+
320
+ end
321
+ end
322
+ end