consul-client 0.1.0
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 +7 -0
- data/README.md +29 -0
- data/consul-client.gemspec +27 -0
- data/lib/consul/client.rb +61 -0
- data/lib/consul/client/http.rb +125 -0
- data/lib/consul/client/local_service.rb +65 -0
- data/lib/consul/client/service.rb +68 -0
- metadata +52 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c1729a5c12181f89e6921f85d96d2df356466a7d
|
4
|
+
data.tar.gz: b52bd63b9e2befc9b9a4d24422195973577362c5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0aa261a9c0055617e26a30af166c99b07da4cef43491f1057a4ef597305ef02041af5f7e073f601b3b4f635df9c5011750496cf7a5bc7598d0b83bb55fd2eec1
|
7
|
+
data.tar.gz: 8201a743e89f3e39a475f41ee1295611b8e6502ce0460c37b939e241c57856783cb09ecc260ee02661274602f27fc3dc2c6e23532f6cc7c7dfdd570906a67240
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
Consul Client
|
2
|
+
=============
|
3
|
+
|
4
|
+
Ruby client library for Consul HTTP API, providing both a thin wrapper around
|
5
|
+
the raw API and higher level behaviours for operating in a Consul environment.
|
6
|
+
|
7
|
+
_This library is experimental! Be sure to thoroughly test and code review
|
8
|
+
before using for anything real._
|
9
|
+
|
10
|
+
Usage
|
11
|
+
-----
|
12
|
+
|
13
|
+
Simple API usage:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
require 'consul/client'
|
17
|
+
|
18
|
+
client = Consul::Client.v1.http
|
19
|
+
client.get("/agent/self")
|
20
|
+
```
|
21
|
+
|
22
|
+
See `example` directory for more:
|
23
|
+
|
24
|
+
* `puts_service.rb` is a minimum server that demostrates coordinated shutdown.
|
25
|
+
* `http_service.rb` builds on top of webrick for an auto-updating server with
|
26
|
+
coordinated restart.
|
27
|
+
|
28
|
+
A `Vagrantfile` is provided that makes three
|
29
|
+
Consul nodes, which is handy for playing around.
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.authors = ["Xavier Shay"]
|
5
|
+
gem.email = ["contact@xaviershay.com"]
|
6
|
+
gem.description =
|
7
|
+
%q{Ruby client library for Consul HTTP API.}
|
8
|
+
gem.summary = %q{
|
9
|
+
Ruby client library for Consul HTTP API, providing both a thin wrapper
|
10
|
+
around the raw API and higher level behaviours for operating in a Consul
|
11
|
+
environment.
|
12
|
+
}
|
13
|
+
gem.homepage = "http://github.com/xaviershay/consul-client"
|
14
|
+
|
15
|
+
gem.executables = []
|
16
|
+
gem.required_ruby_version = '>= 2.1.2'
|
17
|
+
gem.files = Dir.glob("{spec,lib}/**/*.rb") + %w(
|
18
|
+
README.md
|
19
|
+
consul-client.gemspec
|
20
|
+
)
|
21
|
+
gem.test_files = Dir.glob("spec/**/*.rb")
|
22
|
+
gem.name = "consul-client"
|
23
|
+
gem.require_paths = ["lib"]
|
24
|
+
gem.license = "Apache 2.0"
|
25
|
+
gem.version = '0.1.0'
|
26
|
+
gem.has_rdoc = false
|
27
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'consul/client/local_service'
|
2
|
+
require 'consul/client/service'
|
3
|
+
require 'consul/client/http'
|
4
|
+
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
# Top-level Consul name space.
|
8
|
+
module Consul
|
9
|
+
# Top-level client name space. All public entry points are via this module.
|
10
|
+
module Client
|
11
|
+
# Default logger that silences all diagnostic output.
|
12
|
+
NULL_LOGGER = Logger.new("/dev/null")
|
13
|
+
|
14
|
+
# Provides builders that support V1 of the Consul HTTP API.
|
15
|
+
# @return [Consul::Client::V1]
|
16
|
+
def self.v1
|
17
|
+
V1.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Do not instantiate this class directly.
|
21
|
+
#
|
22
|
+
# @see Consul::Client.v1
|
23
|
+
class V1
|
24
|
+
# Returns high-level local service utility functions.
|
25
|
+
#
|
26
|
+
# @param name [String] name of the service. Must match service ID in
|
27
|
+
# Consul.
|
28
|
+
# @param http [Consul::Client::HTTP] http client to use.
|
29
|
+
# @param logger [Logger] logger for diagnostic information.
|
30
|
+
# @return [Consul::Client::LocalService]
|
31
|
+
# @example
|
32
|
+
# local = Consul::Client.v1.local_service('web')
|
33
|
+
# local.coordinated_shutdown! { $healthy = false }
|
34
|
+
def local_service(name, http: http, logger: http.logger)
|
35
|
+
LocalService.new(name, http: http, logger: logger)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns high-level service utility functions.
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# service = Consul::Client.v1.service('web')
|
42
|
+
# service.lock('leader') { puts "I am the cluster leader!" }
|
43
|
+
def service(*args)
|
44
|
+
Service.new(*args)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns a thin wrapper around the Consult HTTP API.
|
48
|
+
#
|
49
|
+
# @param host [String] host of Consul agent.
|
50
|
+
# @param port [Integer] port to connect to Consul agent.
|
51
|
+
# @param logger [Logger] logger for diagnostic information.
|
52
|
+
# @return [Consul::Client::HTTP]
|
53
|
+
# @example
|
54
|
+
# http = Consul::Client.v1.http(logger: Logger.new($stdout))
|
55
|
+
# puts http.get("/get/self")["Member"]["Name"]
|
56
|
+
def http(host: "localhost", port: 8500, logger: NULL_LOGGER)
|
57
|
+
HTTP.new(*args)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Consul
|
6
|
+
module Client
|
7
|
+
# Any non-successful response from Consul will result in this error being
|
8
|
+
# thrown.
|
9
|
+
ResponseException = Class.new(StandardError)
|
10
|
+
|
11
|
+
# Low-level wrapper around consul HTTP api. Do not instantiate this class
|
12
|
+
# directly, instead use the appropriate factory methods.
|
13
|
+
#
|
14
|
+
# @see Consul::Client::V1#http
|
15
|
+
class HTTP
|
16
|
+
# @api private
|
17
|
+
def initialize(host:, port:, logger:)
|
18
|
+
@host = host
|
19
|
+
@port = port
|
20
|
+
@logger = logger
|
21
|
+
end
|
22
|
+
|
23
|
+
# Get JSON data from an endpoint.
|
24
|
+
#
|
25
|
+
# @param request_uri [String] portion of the HTTP path after the version
|
26
|
+
# base, such as +/agent/self+.
|
27
|
+
# @return [Object] parsed JSON response
|
28
|
+
# @raise [ResponseException] if non-200 response is received.
|
29
|
+
def get(request_uri)
|
30
|
+
url = base_uri + request_uri
|
31
|
+
logger.debug("GET #{url}")
|
32
|
+
|
33
|
+
uri = URI.parse(url)
|
34
|
+
|
35
|
+
response = http_request(:get, uri)
|
36
|
+
|
37
|
+
parse_body(response)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Watch an endpoint until the value returned causes the block to evaluate
|
41
|
+
# +false+.
|
42
|
+
#
|
43
|
+
# @param request_uri [String] portion of the HTTP path after the version
|
44
|
+
# base, such as +/agent/self+.
|
45
|
+
# @return [Object] parsed JSON response
|
46
|
+
# @raise [ResponseException] if non-200 response is received.
|
47
|
+
# @example blocks until there are at least 3 passing nodes for the web service.
|
48
|
+
# http.get_while("/health/service/web?passing") do |data|
|
49
|
+
# data.size <= 2
|
50
|
+
# end
|
51
|
+
def get_while(request_uri, &block)
|
52
|
+
url = base_uri + request_uri
|
53
|
+
index = 0
|
54
|
+
json = nil
|
55
|
+
|
56
|
+
check = ->{
|
57
|
+
uri = URI.parse(url)
|
58
|
+
uri.query ||= ""
|
59
|
+
uri.query += "&index=#{index}&wait=10s"
|
60
|
+
logger.debug("GET #{uri}")
|
61
|
+
|
62
|
+
response = http_request(:get, uri)
|
63
|
+
index = response['x-consul-index'].to_i
|
64
|
+
|
65
|
+
json = parse_body(response)
|
66
|
+
|
67
|
+
block.(json)
|
68
|
+
}
|
69
|
+
|
70
|
+
while check.()
|
71
|
+
end
|
72
|
+
|
73
|
+
json
|
74
|
+
end
|
75
|
+
|
76
|
+
# Put request to an endpoint. If data is provided, it is JSON encoded and
|
77
|
+
# sent in the request body.
|
78
|
+
# @param request_uri [String] portion of the HTTP path after the version
|
79
|
+
# base, such as +/agent/self+.
|
80
|
+
# @param data [Object] body for request
|
81
|
+
# @return [Object] parsed JSON response
|
82
|
+
# @raise [ResponseException] if non-200 response is received.
|
83
|
+
def put(request_uri, data = nil)
|
84
|
+
url = base_uri + request_uri
|
85
|
+
logger.debug("PUT #{url}")
|
86
|
+
|
87
|
+
uri = URI.parse(url)
|
88
|
+
|
89
|
+
response = http_request(:put, uri, data)
|
90
|
+
|
91
|
+
parse_body(response)
|
92
|
+
end
|
93
|
+
|
94
|
+
attr_accessor :logger
|
95
|
+
|
96
|
+
protected
|
97
|
+
|
98
|
+
def base_uri
|
99
|
+
"http://#{@host}:#{@port}/v1"
|
100
|
+
end
|
101
|
+
|
102
|
+
def parse_body(response)
|
103
|
+
JSON.parse("[#{response.body}]")[0]
|
104
|
+
end
|
105
|
+
|
106
|
+
def http_request(method, uri, data = nil)
|
107
|
+
method = {
|
108
|
+
get: Net::HTTP::Get,
|
109
|
+
put: Net::HTTP::Put,
|
110
|
+
}.fetch(method)
|
111
|
+
|
112
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
113
|
+
request = method.new(uri.request_uri)
|
114
|
+
request.body = data.to_json if data
|
115
|
+
response = http.request(request)
|
116
|
+
|
117
|
+
if response.code.to_i >= 400
|
118
|
+
raise ResponseException, "#{response.code} on #{uri}"
|
119
|
+
end
|
120
|
+
|
121
|
+
response
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Consul
|
2
|
+
module Client
|
3
|
+
# Provides operations on services running on the local machine. Do not
|
4
|
+
# instantiate this class directly, instead use the appropriate factory
|
5
|
+
# methods.
|
6
|
+
#
|
7
|
+
# @see Consul::Client::V1#local_service
|
8
|
+
class LocalService
|
9
|
+
# @api private
|
10
|
+
def initialize(name, http:, logger:)
|
11
|
+
@name = name
|
12
|
+
@consul = consul
|
13
|
+
consul.logger = logger
|
14
|
+
end
|
15
|
+
|
16
|
+
# Coordinate the shutdown of this node with the rest of the cluster so
|
17
|
+
# that a minimum number of nodes is always healthy. Blocks until a
|
18
|
+
# shutdown lock has been obtained and the cluster is healthy before
|
19
|
+
# yielding, in which callers should mark the service unhealthy (but
|
20
|
+
# continue to accept traffic). After the unhealthy state of the service
|
21
|
+
# has propagated and `grace_period` seconds has passed, this method
|
22
|
+
# returns and the caller should stop accepting new connections, finish
|
23
|
+
# existing work, then terminate.
|
24
|
+
#
|
25
|
+
# @param min_nodes [Integer] minimum require nodes for cluster to be
|
26
|
+
# considered healthy.
|
27
|
+
# @param grace_period [Integer] number of seconds to sleep after service
|
28
|
+
# has been marked unhealthy in the cluster. This is important so
|
29
|
+
# that any in-flight requests are still able to be handled.
|
30
|
+
def coordinated_shutdown!(min_nodes: 1, grace_period: 3, &block)
|
31
|
+
cluster = Consul::Client.v1.service(name, consul: consul)
|
32
|
+
|
33
|
+
cluster.lock("shutdown") do
|
34
|
+
cluster.wait_until_healthy!(min_nodes: min_nodes)
|
35
|
+
block.()
|
36
|
+
wait_until_unhealthy!
|
37
|
+
|
38
|
+
# Release lock here and perform shutdown in our own time, since we
|
39
|
+
# know the consistent view of nodes does not include this one and so
|
40
|
+
# is safe for other nodes to try restarting.
|
41
|
+
end
|
42
|
+
|
43
|
+
# Grace period for any in-flight connections on their way already
|
44
|
+
# before health check failure propagated.
|
45
|
+
#
|
46
|
+
# No way to avoid a sleep here.
|
47
|
+
Kernel.sleep grace_period
|
48
|
+
end
|
49
|
+
|
50
|
+
# Waits until the propagated health of this node is unhealthy so it is
|
51
|
+
# not receiving new traffic.
|
52
|
+
def wait_until_unhealthy!
|
53
|
+
agent = consul.get("/agent/self")["Member"]["Name"]
|
54
|
+
consul.get_while("/health/node/#{agent}") do |data|
|
55
|
+
status = data.detect {|x| x["CheckID"] == "service:#{name}" }["Status"]
|
56
|
+
status == 'passing'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
attr_reader :name, :consul
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Consul
|
2
|
+
module Client
|
3
|
+
# Provides cluster coordination features. Do not instantiate this class
|
4
|
+
# directly, instead use the appropriate factory methods.
|
5
|
+
#
|
6
|
+
# @see Consul::Client::V1#service
|
7
|
+
class Service
|
8
|
+
# @api private
|
9
|
+
def initialize(name, consul: Consul::Client.v1)
|
10
|
+
@name = name
|
11
|
+
@consul = consul
|
12
|
+
end
|
13
|
+
|
14
|
+
# Creates a session tied to this cluster, then blocks indefinitely until
|
15
|
+
# the requested lock can be acquired, then yields. The lock is released
|
16
|
+
# and the session destroyed when the block completes.
|
17
|
+
#
|
18
|
+
# @param key [String] the name of the lock to acquire. This is namespace
|
19
|
+
# under the service name and stored directly in the KV store,
|
20
|
+
# so make sure it does not conflict with other names. For
|
21
|
+
# instance, the leader lock for the +web+ service would be
|
22
|
+
# stored at +/kv/web/leader+.
|
23
|
+
def lock(key, &block)
|
24
|
+
session = consul.put("/session/create",
|
25
|
+
LockDelay: '5s',
|
26
|
+
Checks: ["service:#{name}", "serfHealth"]
|
27
|
+
)["ID"]
|
28
|
+
loop do
|
29
|
+
locked = consul.put("/kv/#{name}/#{key}?acquire=#{session}")
|
30
|
+
|
31
|
+
if locked
|
32
|
+
begin
|
33
|
+
block.call
|
34
|
+
ensure
|
35
|
+
consul.put("/kv/#{name}/#{key}?release=#{session}")
|
36
|
+
consul.put("/session/destroy/#{session}")
|
37
|
+
end
|
38
|
+
return
|
39
|
+
else
|
40
|
+
consul.get_while("/kv/#{name}/#{key}") do |body|
|
41
|
+
body[0]["Session"]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
# TODO: Figure out why long poll doesn't work.
|
45
|
+
# https://gist.github.com/xaviershay/30128b968bde0e2d3e0b/edit
|
46
|
+
sleep 2
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Block indefinitely until the cluster is healthy.
|
51
|
+
#
|
52
|
+
# @param min_nodes [Integer] minimum number of nodes required to be
|
53
|
+
# healthy for the cluster as a whole to be considered healthy.
|
54
|
+
# This method will block until there is at least one more than
|
55
|
+
# this number, assuming that the caller is about to terminate.
|
56
|
+
def wait_until_healthy!(min_nodes: 1)
|
57
|
+
consul.get_while("/health/service/#{name}?passing") do |data|
|
58
|
+
data.size <= min_nodes
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
attr_reader :name
|
65
|
+
attr_reader :consul
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
metadata
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: consul-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Xavier Shay
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-08-17 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Ruby client library for Consul HTTP API.
|
14
|
+
email:
|
15
|
+
- contact@xaviershay.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- README.md
|
21
|
+
- consul-client.gemspec
|
22
|
+
- lib/consul/client.rb
|
23
|
+
- lib/consul/client/http.rb
|
24
|
+
- lib/consul/client/local_service.rb
|
25
|
+
- lib/consul/client/service.rb
|
26
|
+
homepage: http://github.com/xaviershay/consul-client
|
27
|
+
licenses:
|
28
|
+
- Apache 2.0
|
29
|
+
metadata: {}
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: 2.1.2
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
requirements: []
|
45
|
+
rubyforge_project:
|
46
|
+
rubygems_version: 2.2.2
|
47
|
+
signing_key:
|
48
|
+
specification_version: 4
|
49
|
+
summary: Ruby client library for Consul HTTP API, providing both a thin wrapper around
|
50
|
+
the raw API and higher level behaviours for operating in a Consul environment.
|
51
|
+
test_files: []
|
52
|
+
has_rdoc: false
|