consul_syncer 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 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: []