synapse-aurora 0.11.2

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.
Files changed (40) hide show
  1. data/.gitignore +23 -0
  2. data/.mailmap +3 -0
  3. data/.nix/Gemfile.nix +141 -0
  4. data/.nix/rubylibs.nix +42 -0
  5. data/.rspec +2 -0
  6. data/.travis.yml +5 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +22 -0
  9. data/Makefile +6 -0
  10. data/README.md +339 -0
  11. data/Rakefile +8 -0
  12. data/bin/synapse +62 -0
  13. data/config/hostheader_test.json +71 -0
  14. data/config/svcdir_test.json +46 -0
  15. data/config/synapse.conf.json +90 -0
  16. data/config/synapse_services/service1.json +24 -0
  17. data/config/synapse_services/service2.json +24 -0
  18. data/default.nix +66 -0
  19. data/lib/synapse.rb +85 -0
  20. data/lib/synapse/base.rb +5 -0
  21. data/lib/synapse/haproxy.rb +797 -0
  22. data/lib/synapse/log.rb +24 -0
  23. data/lib/synapse/service_watcher.rb +36 -0
  24. data/lib/synapse/service_watcher/base.rb +109 -0
  25. data/lib/synapse/service_watcher/dns.rb +109 -0
  26. data/lib/synapse/service_watcher/docker.rb +120 -0
  27. data/lib/synapse/service_watcher/ec2tag.rb +133 -0
  28. data/lib/synapse/service_watcher/zookeeper.rb +153 -0
  29. data/lib/synapse/service_watcher/zookeeper_aurora.rb +76 -0
  30. data/lib/synapse/service_watcher/zookeeper_dns.rb +232 -0
  31. data/lib/synapse/version.rb +3 -0
  32. data/spec/lib/synapse/haproxy_spec.rb +32 -0
  33. data/spec/lib/synapse/service_watcher_base_spec.rb +55 -0
  34. data/spec/lib/synapse/service_watcher_docker_spec.rb +152 -0
  35. data/spec/lib/synapse/service_watcher_ec2tags_spec.rb +220 -0
  36. data/spec/spec_helper.rb +22 -0
  37. data/spec/support/configuration.rb +9 -0
  38. data/spec/support/minimum.conf.yaml +27 -0
  39. data/synapse.gemspec +33 -0
  40. metadata +227 -0
@@ -0,0 +1,24 @@
1
+ module Synapse
2
+ module Logging
3
+
4
+ def log
5
+ @logger ||= Logging.logger_for(self.class.name)
6
+ end
7
+
8
+ # Use a hash class-ivar to cache a unique Logger per class:
9
+ @loggers = {}
10
+
11
+ class << self
12
+ def logger_for(classname)
13
+ @loggers[classname] ||= configure_logger_for(classname)
14
+ end
15
+
16
+ def configure_logger_for(classname)
17
+ logger = Logger.new(STDERR)
18
+ logger.level = Logger::INFO unless ENV['DEBUG']
19
+ logger.progname = classname
20
+ return logger
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,36 @@
1
+ require "synapse/service_watcher/base"
2
+ require "synapse/service_watcher/zookeeper"
3
+ require "synapse/service_watcher/ec2tag"
4
+ require "synapse/service_watcher/dns"
5
+ require "synapse/service_watcher/docker"
6
+ require "synapse/service_watcher/zookeeper_dns"
7
+ require "synapse/service_watcher/zookeeper_aurora"
8
+
9
+ module Synapse
10
+ class ServiceWatcher
11
+
12
+ @watchers = {
13
+ 'base' => BaseWatcher,
14
+ 'zookeeper' => ZookeeperWatcher,
15
+ 'ec2tag' => EC2Watcher,
16
+ 'dns' => DnsWatcher,
17
+ 'docker' => DockerWatcher,
18
+ 'zookeeper_dns' => ZookeeperDnsWatcher,
19
+ 'zookeeper_aurora' => ZookeeperAuroraWatcher
20
+ }
21
+
22
+ # the method which actually dispatches watcher creation requests
23
+ def self.create(name, opts, synapse)
24
+ opts['name'] = name
25
+
26
+ raise ArgumentError, "Missing discovery method when trying to create watcher" \
27
+ unless opts.has_key?('discovery') && opts['discovery'].has_key?('method')
28
+
29
+ discovery_method = opts['discovery']['method']
30
+ raise ArgumentError, "Invalid discovery method #{discovery_method}" \
31
+ unless @watchers.has_key?(discovery_method)
32
+
33
+ return @watchers[discovery_method].new(opts, synapse)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,109 @@
1
+ require 'synapse/log'
2
+
3
+ module Synapse
4
+ class BaseWatcher
5
+ include Logging
6
+
7
+ LEADER_WARN_INTERVAL = 30
8
+
9
+ attr_reader :name, :haproxy
10
+
11
+ def initialize(opts={}, synapse)
12
+ super()
13
+
14
+ @synapse = synapse
15
+
16
+ # set required service parameters
17
+ %w{name discovery haproxy}.each do |req|
18
+ raise ArgumentError, "missing required option #{req}" unless opts[req]
19
+ end
20
+
21
+ @name = opts['name']
22
+ @discovery = opts['discovery']
23
+
24
+ @leader_election = opts['leader_election'] || false
25
+ @leader_last_warn = Time.now - LEADER_WARN_INTERVAL
26
+
27
+ # the haproxy config
28
+ @haproxy = opts['haproxy']
29
+ @haproxy['server_options'] ||= ""
30
+ @haproxy['server_port_override'] ||= nil
31
+ %w{backend frontend listen}.each do |sec|
32
+ @haproxy[sec] ||= []
33
+ end
34
+
35
+ unless @haproxy.include?('port')
36
+ log.warn "synapse: service #{name}: haproxy config does not include a port; only backend sections for the service will be created; you must move traffic there manually using configuration in `extra_sections`"
37
+ end
38
+
39
+ # set initial backends to default servers, if any
40
+ @default_servers = opts['default_servers'] || []
41
+ @backends = @default_servers
42
+
43
+ @keep_default_servers = opts['keep_default_servers'] || false
44
+
45
+ # set a flag used to tell the watchers to exit
46
+ # this is not used in every watcher
47
+ @should_exit = false
48
+
49
+ validate_discovery_opts
50
+ end
51
+
52
+ # this should be overridden to actually start your watcher
53
+ def start
54
+ log.info "synapse: starting stub watcher; this means doing nothing at all!"
55
+ end
56
+
57
+ # this should be overridden to actually stop your watcher if necessary
58
+ # if you are running a thread, your loop should run `until @should_exit`
59
+ def stop
60
+ log.info "synapse: stopping watcher #{self.name} using default stop handler"
61
+ @should_exit = true
62
+ end
63
+
64
+ # this should be overridden to do a health check of the watcher
65
+ def ping?
66
+ true
67
+ end
68
+
69
+ def backends
70
+ if @leader_election
71
+ if @backends.all?{|b| b.key?('id') && b['id']}
72
+ smallest = @backends.sort_by{ |b| b['id']}.first
73
+ log.debug "synapse: leader election chose one of #{@backends.count} backends " \
74
+ "(#{smallest['host']}:#{smallest['port']} with id #{smallest['id']})"
75
+
76
+ return [smallest]
77
+ elsif (Time.now - @leader_last_warn) > LEADER_WARN_INTERVAL
78
+ log.warn "synapse: service #{@name}: leader election failed; not all backends include an id"
79
+ @leader_last_warn = Time.now
80
+ end
81
+
82
+ # if leader election fails, return no backends
83
+ return []
84
+ end
85
+
86
+ return @backends
87
+ end
88
+
89
+ private
90
+ def validate_discovery_opts
91
+ raise ArgumentError, "invalid discovery method '#{@discovery['method']}' for base watcher" \
92
+ unless @discovery['method'] == 'base'
93
+
94
+ log.warn "synapse: warning: a stub watcher with no default servers is pretty useless" if @default_servers.empty?
95
+ end
96
+
97
+ def set_backends(new_backends)
98
+ if @keep_default_servers
99
+ @backends = @default_servers + new_backends
100
+ else
101
+ @backends = new_backends
102
+ end
103
+ end
104
+
105
+ def reconfigure!
106
+ @synapse.reconfigure!
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,109 @@
1
+ require "synapse/service_watcher/base"
2
+
3
+ require 'thread'
4
+ require 'resolv'
5
+
6
+ module Synapse
7
+ class DnsWatcher < BaseWatcher
8
+ def start
9
+ @check_interval = @discovery['check_interval'] || 30.0
10
+ @nameserver = @discovery['nameserver']
11
+
12
+ @watcher = Thread.new do
13
+ watch
14
+ end
15
+ end
16
+
17
+ def ping?
18
+ @watcher.alive? && !(resolver.getaddresses('airbnb.com').empty?)
19
+ end
20
+
21
+ def discovery_servers
22
+ @discovery['servers']
23
+ end
24
+
25
+ private
26
+ def validate_discovery_opts
27
+ raise ArgumentError, "invalid discovery method #{@discovery['method']}" \
28
+ unless @discovery['method'] == 'dns'
29
+ raise ArgumentError, "a non-empty list of servers is required" \
30
+ if discovery_servers.empty?
31
+ end
32
+
33
+ def watch
34
+ last_resolution = resolve_servers
35
+ configure_backends(last_resolution)
36
+ until @should_exit
37
+ begin
38
+ start = Time.now
39
+ current_resolution = resolve_servers
40
+ unless last_resolution == current_resolution
41
+ last_resolution = current_resolution
42
+ configure_backends(last_resolution)
43
+ end
44
+
45
+ sleep_until_next_check(start)
46
+ rescue => e
47
+ log.warn "Error in watcher thread: #{e.inspect}"
48
+ log.warn e.backtrace
49
+ end
50
+ end
51
+
52
+ log.info "synapse: dns watcher exited successfully"
53
+ end
54
+
55
+ def sleep_until_next_check(start_time)
56
+ sleep_time = @check_interval - (Time.now - start_time)
57
+ if sleep_time > 0.0
58
+ sleep(sleep_time)
59
+ end
60
+ end
61
+
62
+ def resolve_servers
63
+ resolver.tap do |dns|
64
+ resolution = discovery_servers.map do |server|
65
+ addresses = dns.getaddresses(server['host']).map(&:to_s)
66
+ [server, addresses.sort]
67
+ end
68
+
69
+ return resolution
70
+ end
71
+ rescue => e
72
+ log.warn "Error while resolving host names: #{e.inspect}"
73
+ []
74
+ end
75
+
76
+ def resolver
77
+ args = [{:nameserver => @nameserver}] if @nameserver
78
+ Resolv::DNS.open(*args)
79
+ end
80
+
81
+ def configure_backends(servers)
82
+ new_backends = servers.flat_map do |(server, addresses)|
83
+ addresses.map do |address|
84
+ {
85
+ 'host' => address,
86
+ 'port' => server['port'],
87
+ 'name' => server['name'],
88
+ }
89
+ end
90
+ end
91
+
92
+ if new_backends.empty?
93
+ if @default_servers.empty?
94
+ log.warn "synapse: no backends and no default servers for service #{@name};" \
95
+ " using previous backends: #{@backends.inspect}"
96
+ else
97
+ log.warn "synapse: no backends for service #{@name};" \
98
+ " using default servers: #{@default_servers.inspect}"
99
+ @backends = @default_servers
100
+ end
101
+ else
102
+ log.info "synapse: discovered #{new_backends.length} backends for service #{@name}"
103
+ set_backends(new_backends)
104
+ end
105
+
106
+ reconfigure!
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,120 @@
1
+ require "synapse/service_watcher/base"
2
+ require 'docker'
3
+
4
+ module Synapse
5
+ class DockerWatcher < BaseWatcher
6
+ def start
7
+ @check_interval = @discovery['check_interval'] || 15.0
8
+ @watcher = Thread.new do
9
+ watch
10
+ end
11
+ end
12
+
13
+ private
14
+ def validate_discovery_opts
15
+ raise ArgumentError, "invalid discovery method #{@discovery['method']}" \
16
+ unless @discovery['method'] == 'docker'
17
+ raise ArgumentError, "a non-empty list of servers is required" \
18
+ if @discovery['servers'].nil? or @discovery['servers'].empty?
19
+ raise ArgumentError, "non-empty image_name required" \
20
+ if @discovery['image_name'].nil? or @discovery['image_name'].empty?
21
+ raise ArgumentError, "container_port required" \
22
+ if @discovery['container_port'].nil?
23
+ end
24
+
25
+ def watch
26
+ last_containers = []
27
+ until @should_exit
28
+ begin
29
+ start = Time.now
30
+ current_containers = containers
31
+ unless last_containers == current_containers
32
+ last_containers = current_containers
33
+ configure_backends(last_containers)
34
+ end
35
+
36
+ sleep_until_next_check(start)
37
+ rescue Exception => e
38
+ log.warn "synapse: error in watcher thread: #{e.inspect}"
39
+ log.warn e.backtrace
40
+ end
41
+ end
42
+
43
+ log.info "synapse: docker watcher exited successfully"
44
+ end
45
+
46
+ def sleep_until_next_check(start_time)
47
+ sleep_time = @check_interval - (Time.now - start_time)
48
+ if sleep_time > 0.0
49
+ sleep(sleep_time)
50
+ end
51
+ end
52
+
53
+ def rewrite_container_ports(ports)
54
+ pairs = []
55
+ if ports.is_a?(String)
56
+ # "Ports" comes through (as of 0.6.5) as a string like "0.0.0.0:49153->6379/tcp, 0.0.0.0:49153->6379/tcp"
57
+ # Convert string to a map of container port to host port: {"7000"->"49158", "6379": "49159"}
58
+ pairs = ports.split(", ").collect do |v|
59
+ pair = v.split('->')
60
+ [ pair[1].rpartition("/").first, pair[0].rpartition(":").last ]
61
+ end
62
+ elsif ports.is_a?(Array)
63
+ # New style API, ports is an array of hashes, with numeric values (or nil if no ports forwarded)
64
+ pairs = ports.collect do |v|
65
+ [v['PrivatePort'].to_s, v['PublicPort'].to_s]
66
+ end
67
+ end
68
+ Hash[pairs]
69
+ end
70
+
71
+ def containers
72
+ backends = @discovery['servers'].map do |server|
73
+ Docker.url = "http://#{server['host']}:#{server['port'] || 4243}"
74
+ begin
75
+ cnts = Docker::Util.parse_json(Docker.connection.get('/containers/json', {}))
76
+ rescue => e
77
+ log.warn "synapse: error polling docker host #{Docker.url}: #{e.inspect}"
78
+ next []
79
+ end
80
+ cnts.each do |cnt|
81
+ cnt['Ports'] = rewrite_container_ports cnt['Ports']
82
+ end
83
+ # Discover containers that match the image/port we're interested in
84
+ cnts = cnts.find_all do |cnt|
85
+ cnt["Image"].rpartition(":").first == @discovery["image_name"] \
86
+ and cnt["Ports"].has_key?(@discovery["container_port"].to_s())
87
+ end
88
+ cnts.map do |cnt|
89
+ {
90
+ 'name' => server['name'],
91
+ 'host' => server['host'],
92
+ 'port' => cnt["Ports"][@discovery["container_port"].to_s()]
93
+ }
94
+ end
95
+ end
96
+ backends.flatten
97
+ rescue => e
98
+ log.warn "synapse: error while polling for containers: #{e.inspect}"
99
+ []
100
+ end
101
+
102
+ def configure_backends(new_backends)
103
+ if new_backends.empty?
104
+ if @default_servers.empty?
105
+ log.warn "synapse: no backends and no default servers for service #{@name};" \
106
+ " using previous backends: #{@backends.inspect}"
107
+ else
108
+ log.warn "synapse: no backends for service #{@name};" \
109
+ " using default servers: #{@default_servers.inspect}"
110
+ @backends = @default_servers
111
+ end
112
+ else
113
+ log.info "synapse: discovered #{new_backends.length} backends for service #{@name}"
114
+ set_backends(new_backends)
115
+ end
116
+ reconfigure!
117
+ end
118
+
119
+ end
120
+ end
@@ -0,0 +1,133 @@
1
+ require 'synapse/service_watcher/base'
2
+ require 'aws-sdk'
3
+
4
+ module Synapse
5
+ class EC2Watcher < BaseWatcher
6
+
7
+ attr_reader :check_interval
8
+
9
+ def start
10
+ region = @discovery['aws_region'] || ENV['AWS_REGION']
11
+ log.info "Connecting to EC2 region: #{region}"
12
+
13
+ @ec2 = AWS::EC2.new(
14
+ region: region,
15
+ access_key_id: @discovery['aws_access_key_id'] || ENV['AWS_ACCESS_KEY_ID'],
16
+ secret_access_key: @discovery['aws_secret_access_key'] || ENV['AWS_SECRET_ACCESS_KEY'] )
17
+
18
+ @check_interval = @discovery['check_interval'] || 15.0
19
+
20
+ log.info "synapse: ec2tag watcher looking for instances " +
21
+ "tagged with #{@discovery['tag_name']}=#{@discovery['tag_value']}"
22
+
23
+ @watcher = Thread.new { watch }
24
+ end
25
+
26
+ private
27
+
28
+ def validate_discovery_opts
29
+ # Required, via options only.
30
+ raise ArgumentError, "invalid discovery method #{@discovery['method']}" \
31
+ unless @discovery['method'] == 'ec2tag'
32
+ raise ArgumentError, "aws tag name is required for service #{@name}" \
33
+ unless @discovery['tag_name']
34
+ raise ArgumentError, "aws tag value required for service #{@name}" \
35
+ unless @discovery['tag_value']
36
+
37
+ # As we're only looking up instances with hostnames/IPs, need to
38
+ # be explicitly told which port the service we're balancing for listens on.
39
+ unless @haproxy['server_port_override']
40
+ raise ArgumentError,
41
+ "Missing server_port_override for service #{@name} - which port are backends listening on?"
42
+ end
43
+
44
+ unless @haproxy['server_port_override'].match(/^\d+$/)
45
+ raise ArgumentError, "Invalid server_port_override value"
46
+ end
47
+
48
+ # Required, but can use well-known environment variables.
49
+ %w[aws_access_key_id aws_secret_access_key aws_region].each do |attr|
50
+ unless (@discovery[attr] || ENV[attr.upcase])
51
+ raise ArgumentError, "Missing #{attr} option or #{attr.upcase} environment variable"
52
+ end
53
+ end
54
+ end
55
+
56
+ def watch
57
+ last_backends = []
58
+ until @should_exit
59
+ begin
60
+ start = Time.now
61
+ current_backends = discover_instances
62
+
63
+ if last_backends != current_backends
64
+ log.info "synapse: ec2tag watcher backends have changed."
65
+ last_backends = current_backends
66
+ configure_backends(current_backends)
67
+ else
68
+ log.info "synapse: ec2tag watcher backends are unchanged."
69
+ end
70
+
71
+ sleep_until_next_check(start)
72
+ rescue Exception => e
73
+ log.warn "synapse: error in ec2tag watcher thread: #{e.inspect}"
74
+ log.warn e.backtrace
75
+ end
76
+ end
77
+
78
+ log.info "synapse: ec2tag watcher exited successfully"
79
+ end
80
+
81
+ def sleep_until_next_check(start_time)
82
+ sleep_time = check_interval - (Time.now - start_time)
83
+ if sleep_time > 0.0
84
+ sleep(sleep_time)
85
+ end
86
+ end
87
+
88
+ def discover_instances
89
+ AWS.memoize do
90
+ instances = instances_with_tags(@discovery['tag_name'], @discovery['tag_value'])
91
+
92
+ new_backends = []
93
+
94
+ # choice of private_dns_name, dns_name, private_ip_address or
95
+ # ip_address, for now, just stick with the private fields.
96
+ instances.each do |instance|
97
+ new_backends << {
98
+ 'name' => instance.private_dns_name,
99
+ 'host' => instance.private_ip_address,
100
+ 'port' => @haproxy['server_port_override'],
101
+ }
102
+ end
103
+
104
+ new_backends
105
+ end
106
+ end
107
+
108
+ def instances_with_tags(tag_name, tag_value)
109
+ @ec2.instances
110
+ .tagged(tag_name)
111
+ .tagged_values(tag_value)
112
+ .select { |i| i.status == :running }
113
+ end
114
+
115
+ def configure_backends(new_backends)
116
+ if new_backends.empty?
117
+ if @default_servers.empty?
118
+ log.warn "synapse: no backends and no default servers for service #{@name};" \
119
+ " using previous backends: #{@backends.inspect}"
120
+ else
121
+ log.warn "synapse: no backends for service #{@name};" \
122
+ " using default servers: #{@default_servers.inspect}"
123
+ @backends = @default_servers
124
+ end
125
+ else
126
+ log.info "synapse: discovered #{new_backends.length} backends for service #{@name}"
127
+ @backends = new_backends
128
+ end
129
+ @synapse.reconfigure!
130
+ end
131
+ end
132
+ end
133
+