consul_syncer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|