consul_syncer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 84c90cae567535c20d3965ef9bf933ec3a6408e9
4
+ data.tar.gz: c757284962593f66d365345220e87f33d082cf65
5
+ SHA512:
6
+ metadata.gz: 2456d0129b4ce655b3c8d829c20c18e77685e263c61f3c5919f20c360d5fa92316bfcdc4c64a4c46e74631e896d24fe3e9dd17bf689e20f9a49a4fb64945c492
7
+ data.tar.gz: 9320fb542f405a9f28701ad2983c6053ae1bf2d0885cf0bae710571acfd4cde8e8c85b89c9e6c5fabbe33914c907d29fa74929cf64073a590537ad9d3a059258
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (C) 2013 Michael Grosser <michael@grosser.it>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+ require 'faraday'
3
+ require 'logger'
4
+ require 'consul_syncer/endpoint'
5
+ require 'consul_syncer/wrapper'
6
+
7
+ # syncs a given list of endpoints into consul
8
+ # - adds missing
9
+ # - updates changed
10
+ # - removes deprecated
11
+ class ConsulSyncer
12
+ def initialize(url, logger: Logger.new(STDOUT))
13
+ @consul = Wrapper.new(Faraday.new(url))
14
+ @logger = logger
15
+ end
16
+
17
+ # changing tags means all previous services need to be removed manually since
18
+ # they can no longer be found
19
+ def sync(expected_definitions, tags)
20
+ raise ArgumentError, "Need at least 1 tag to reliably update endpoints" if tags.empty?
21
+
22
+ modified = 0
23
+
24
+ # ensure consistent tags to find the endpoints after adding
25
+ expected_definitions = expected_definitions.dup
26
+ expected_definitions.each do |d|
27
+ d[:tags] += tags
28
+ d[:tags].sort!
29
+ end
30
+
31
+ actual_definitions = consul_endpoints(tags).map do |consul_endpoint|
32
+ {
33
+ node: consul_endpoint.node,
34
+ address: consul_endpoint.ip,
35
+ service: consul_endpoint.name,
36
+ service_id: consul_endpoint.service_id,
37
+ tags: consul_endpoint.tags.sort,
38
+ port: consul_endpoint.port
39
+ }
40
+ end
41
+
42
+ identifying = [:node, :service]
43
+ interesting = [*identifying, :address, :tags, :port]
44
+
45
+ expected_definitions.each do |expected|
46
+ description = "#{expected.fetch(:service)} on #{expected.fetch(:node)} in Consul"
47
+
48
+ if remove_matching_service!(actual_definitions, expected, interesting)
49
+ @logger.info "Found #{description}"
50
+ elsif remove_matching_service!(actual_definitions, expected, identifying)
51
+ @logger.info "Updating #{description}"
52
+ modified += 1
53
+ register expected
54
+ else
55
+ @logger.info "Adding #{description}"
56
+ modified += 1
57
+ register expected
58
+ end
59
+ end
60
+
61
+ # all definitions that are left did not match any expected definitions and are no longer needed
62
+ actual_definitions.each do |actual|
63
+ @logger.info "Removing #{actual.fetch(:service)} on #{actual.fetch(:node)} in Consul"
64
+ modified += 1
65
+ deregister actual.fetch(:node), actual.fetch(:service_id)
66
+ end
67
+
68
+ modified
69
+ end
70
+
71
+ private
72
+
73
+ def consul_endpoints(requested_tags)
74
+ services = @consul.request(:get, "/v1/catalog/services?tag=#{requested_tags.first}")
75
+ services.each_with_object([]) do |(name, tags), all|
76
+ # cannot query for multiple tags via query, so handle multi-matching manually
77
+ next if (requested_tags - tags).any?
78
+
79
+ @logger.info "Getting service endpoints for #{name}"
80
+ # this also finds the 'external services' we define since they have no checks
81
+ endpoints = @consul.request(:get, "/v1/health/service/#{name}")
82
+ endpoints.each do |endpoint|
83
+ endpoint = Endpoint.new(endpoint)
84
+ next if (requested_tags - endpoint.tags).any?
85
+ all << endpoint
86
+ end
87
+ end
88
+ end
89
+
90
+ def remove_matching_service!(actuals, expected, keys)
91
+ return unless found = actuals.detect { |actual| actual.values_at(*keys) == expected.values_at(*keys) }
92
+ actuals.delete(found)
93
+ end
94
+
95
+ # creates or updates based on node and service
96
+ def register(node:, service:, address:, tags:, port:)
97
+ @consul.request(
98
+ :put,
99
+ '/v1/catalog/register',
100
+ Node: node,
101
+ Address: address,
102
+ Service: {
103
+ Service: service,
104
+ Tags: tags,
105
+ Port: port
106
+ }
107
+ )
108
+ end
109
+
110
+ def deregister(node, service_id)
111
+ @consul.request(
112
+ :put,
113
+ '/v1/catalog/deregister',
114
+ Node: node,
115
+ ServiceID: service_id
116
+ )
117
+ end
118
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ class ConsulSyncer
3
+ class Endpoint
4
+ def initialize(service_hash)
5
+ @hash = service_hash
6
+ end
7
+
8
+ def name
9
+ @hash.fetch('Service').fetch('Service')
10
+ end
11
+
12
+ def service_id
13
+ @hash.fetch('Service').fetch('ID')
14
+ end
15
+
16
+ def node
17
+ @hash.fetch('Node').fetch('Node')
18
+ end
19
+
20
+ def port
21
+ @hash.fetch('Service').fetch('Port')
22
+ end
23
+
24
+ def tags
25
+ @hash.fetch('Service').fetch('Tags', [])
26
+ end
27
+
28
+ def ip
29
+ @hash.fetch('Node').fetch('Address')
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ class ConsulSyncer
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+ # - parses json responses
3
+ # - fails with descriptive output when a request fails
4
+ class ConsulSyncer
5
+ class Wrapper
6
+ BACKOFF = [0.1, 0.5, 1.0, 2.0].freeze
7
+
8
+ class ConsulError < StandardError
9
+ end
10
+
11
+ def initialize(consul)
12
+ @consul = consul
13
+ end
14
+
15
+ def request(method, path, payload = nil)
16
+ retry_on_error do
17
+ args = [path]
18
+ args << payload.to_json if payload
19
+ response = @consul.send(method, *args)
20
+ if response.status == 200
21
+ if method == :get
22
+ JSON.parse(response.body)
23
+ else
24
+ true
25
+ end
26
+ else
27
+ raise(
28
+ ConsulError,
29
+ "Failed to request #{response.env.method} #{response.env.url}: #{response.status} -- #{response.body}"
30
+ )
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def retry_on_error
38
+ yield
39
+ rescue Faraday::Error, ConsulError
40
+ retried ||= 0
41
+ backoff = BACKOFF[retried]
42
+ raise unless backoff
43
+ retried += 1
44
+
45
+ warn "Consul request failed, retrying in #{backoff}s"
46
+ sleep backoff
47
+ retry
48
+ end
49
+ end
50
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: consul_syncer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Grosser
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-01-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description:
28
+ email: michael@grosser.it
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - MIT-LICENSE
34
+ - lib/consul_syncer.rb
35
+ - lib/consul_syncer/endpoint.rb
36
+ - lib/consul_syncer/version.rb
37
+ - lib/consul_syncer/wrapper.rb
38
+ homepage: https://github.com/grosser/consul_syncer
39
+ licenses:
40
+ - MIT
41
+ metadata: {}
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 2.0.0
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubyforge_project:
58
+ rubygems_version: 2.5.1
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Sync remote services into consul
62
+ test_files: []