consul-client 0.1.0

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