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 +7 -0
- data/MIT-LICENSE +20 -0
- data/lib/consul_syncer.rb +118 -0
- data/lib/consul_syncer/endpoint.rb +32 -0
- data/lib/consul_syncer/version.rb +3 -0
- data/lib/consul_syncer/wrapper.rb +50 -0
- metadata +62 -0
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,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: []
|