synapse 0.8.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 1.9.3
5
+
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- synapse (0.8.0)
4
+ synapse (0.9.1)
5
5
  docker-api (~> 1.7.2)
6
6
  zk (~> 1.9.2)
7
7
 
@@ -27,6 +27,7 @@ GEM
27
27
  slop (~> 3.4)
28
28
  pry-nav (0.2.3)
29
29
  pry (~> 0.9.10)
30
+ rake (10.1.1)
30
31
  rspec (2.14.1)
31
32
  rspec-core (~> 2.14.0)
32
33
  rspec-expectations (~> 2.14.0)
@@ -36,10 +37,10 @@ GEM
36
37
  diff-lcs (>= 1.1.3, < 2.0)
37
38
  rspec-mocks (2.14.3)
38
39
  slop (3.4.6)
39
- zk (1.9.2)
40
+ zk (1.9.3)
40
41
  logging (~> 1.7.2)
41
42
  zookeeper (~> 1.4.0)
42
- zookeeper (1.4.7)
43
+ zookeeper (1.4.8)
43
44
 
44
45
  PLATFORMS
45
46
  ruby
@@ -47,5 +48,6 @@ PLATFORMS
47
48
  DEPENDENCIES
48
49
  pry
49
50
  pry-nav
51
+ rake
50
52
  rspec
51
53
  synapse!
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Build Status](https://travis-ci.org/airbnb/synapse.png?branch=master)](https://travis-ci.org/airbnb/synapse)
2
+
1
3
  # Synapse #
2
4
 
3
5
  Synapse is Airbnb's new system for service discovery.
data/Rakefile CHANGED
@@ -1 +1,8 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :test => :spec
7
+ task :default => :spec
8
+
data/bin/synapse CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'json'
3
+ require 'yaml'
4
4
  require 'optparse'
5
5
 
6
6
  require 'synapse'
@@ -32,15 +32,15 @@ optparse.parse!
32
32
  def parseconfig(filename)
33
33
  # parse synapse config file
34
34
  begin
35
- c = JSON::parse(File.read(filename))
35
+ c = YAML::parse(File.read(filename))
36
36
  rescue Errno::ENOENT => e
37
37
  raise ArgumentError, "config file does not exist:\n#{e.inspect}"
38
38
  rescue Errno::EACCES => e
39
39
  raise ArgumentError, "could not open config file:\n#{e.inspect}"
40
- rescue JSON::ParserError => e
41
- raise "config file #{filename} is not json:\n#{e.inspect}"
40
+ rescue YAML::ParseError => e
41
+ raise "config file #{filename} is not yaml:\n#{e.inspect}"
42
42
  end
43
- return c
43
+ return c.to_ruby
44
44
  end
45
45
 
46
46
  config = parseconfig(options[:config])
@@ -51,8 +51,8 @@ if config.has_key?('service_conf_dir')
51
51
  if ! Dir.exists?(cdir)
52
52
  raise "service conf dir does not exist:#{cdir}"
53
53
  end
54
- cfiles = Dir.glob(File.join(cdir, '*.json'))
55
- cfiles.each { |x| config['services'][File.basename(x[/(.*)\.json$/, 1])] = parseconfig(x) }
54
+ cfiles = Dir.glob(File.join(cdir, '*.{json,yaml}'))
55
+ cfiles.each { |x| config['services'][File.basename(x[/(.*)\.(json|yaml)$/, 1])] = parseconfig(x) }
56
56
  end
57
57
 
58
58
  # run synapse
@@ -1,7 +1,5 @@
1
1
  require 'synapse/log'
2
-
3
2
  require 'socket'
4
- require 'digest'
5
3
 
6
4
  module Synapse
7
5
  class Haproxy
@@ -750,8 +748,12 @@ module Synapse
750
748
 
751
749
  # used to build unique, consistent haproxy names for backends
752
750
  def construct_name(backend)
753
- address_digest = Digest::SHA256.hexdigest(backend['host'])[0..7]
754
- return "#{backend['name']}:#{backend['port']}_#{address_digest}"
751
+ name = "#{backend['host']}:#{backend['port']}"
752
+ if backend['name'] && !backend['name'].empty?
753
+ name = "#{name}_#{backend['name']}"
754
+ end
755
+
756
+ return name
755
757
  end
756
758
  end
757
759
  end
@@ -1,6 +1,12 @@
1
+ require 'synapse/log'
2
+
1
3
  module Synapse
2
4
  class BaseWatcher
3
- attr_reader :name, :backends, :haproxy
5
+ include Logging
6
+
7
+ LEADER_WARN_INTERVAL = 30
8
+
9
+ attr_reader :name, :haproxy
4
10
 
5
11
  def initialize(opts={}, synapse)
6
12
  super()
@@ -15,6 +21,9 @@ module Synapse
15
21
  @name = opts['name']
16
22
  @discovery = opts['discovery']
17
23
 
24
+ @leader_election = opts['leader_election'] || false
25
+ @leader_last_warn = Time.now - LEADER_WARN_INTERVAL
26
+
18
27
  # the haproxy config
19
28
  @haproxy = opts['haproxy']
20
29
  @haproxy['server_options'] ||= ""
@@ -55,6 +64,26 @@ module Synapse
55
64
  true
56
65
  end
57
66
 
67
+ def backends
68
+ if @leader_election
69
+ if @backends.all?{|b| b.key?('id') && b['id']}
70
+ smallest = @backends.sort_by{ |b| b['id']}.first
71
+ log.debug "synapse: leader election chose one of #{@backends.count} backends " \
72
+ "(#{smallest['host']}:#{smallest['port']} with id #{smallest['id']})"
73
+
74
+ return [smallest]
75
+ elsif (Time.now - @leader_last_warn) > LEADER_WARN_INTERVAL
76
+ log.warn "synapse: service #{@name}: leader election failed; not all backends include an id"
77
+ @leader_last_warn = Time.now
78
+ end
79
+
80
+ # if leader election fails, return no backends
81
+ return []
82
+ end
83
+
84
+ return @backends
85
+ end
86
+
58
87
  private
59
88
  def validate_discovery_opts
60
89
  raise ArgumentError, "invalid discovery method '#{@discovery['method']}' for base watcher" \
@@ -1,12 +1,10 @@
1
1
  require "synapse/service_watcher/base"
2
- require "synapse/log"
3
2
 
4
3
  require 'thread'
5
4
  require 'resolv'
6
5
 
7
6
  module Synapse
8
7
  class DnsWatcher < BaseWatcher
9
- include Logging
10
8
  def start
11
9
  @check_interval = @discovery['check_interval'] || 30.0
12
10
  @nameserver = @discovery['nameserver']
@@ -80,7 +78,6 @@ module Synapse
80
78
  new_backends = servers.flat_map do |(server, addresses)|
81
79
  addresses.map do |address|
82
80
  {
83
- 'name' => server['name'],
84
81
  'host' => address,
85
82
  'port' => server['port']
86
83
  }
@@ -34,7 +34,7 @@ module Synapse
34
34
  end
35
35
 
36
36
  sleep_until_next_check(start)
37
- rescue => e
37
+ rescue Exception => e
38
38
  log.warn "synapse: error in watcher thread: #{e.inspect}"
39
39
  log.warn e.backtrace
40
40
  end
@@ -50,6 +50,24 @@ module Synapse
50
50
  end
51
51
  end
52
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
+
53
71
  def containers
54
72
  backends = @discovery['servers'].map do |server|
55
73
  Docker.url = "http://#{server['host']}:#{server['port'] || 4243}"
@@ -59,14 +77,8 @@ module Synapse
59
77
  log.warn "synapse: error polling docker host #{Docker.url}: #{e.inspect}"
60
78
  next []
61
79
  end
62
- # "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"
63
- # Convert string to a map of container port to host port: {"7000"->"49158", "6379": "49159"}
64
80
  cnts.each do |cnt|
65
- pairs = cnt["Ports"].split(", ").collect do |v|
66
- pair = v.split('->')
67
- [ pair[1].rpartition("/").first, pair[0].rpartition(":").last ]
68
- end
69
- cnt["Ports"] = Hash[pairs]
81
+ cnt['Ports'] = rewrite_container_ports cnt['Ports']
70
82
  end
71
83
  # Discover containers that match the image/port we're interested in
72
84
  cnts = cnts.find_all do |cnt|
@@ -1,11 +1,11 @@
1
1
  require "synapse/service_watcher/base"
2
- require "synapse/log"
3
2
 
4
3
  require 'zk'
5
4
 
6
5
  module Synapse
7
6
  class ZookeeperWatcher < BaseWatcher
8
- include Logging
7
+ NUMBERS_RE = /^\d+$/
8
+
9
9
  def start
10
10
  zk_hosts = @discovery['hosts'].shuffle.join(',')
11
11
 
@@ -55,18 +55,22 @@ module Synapse
55
55
 
56
56
  new_backends = []
57
57
  begin
58
- @zk.children(@discovery['path'], :watch => true).map do |name|
59
- node = @zk.get("#{@discovery['path']}/#{name}")
58
+ @zk.children(@discovery['path'], :watch => true).each do |id|
59
+ node = @zk.get("#{@discovery['path']}/#{id}")
60
60
 
61
61
  begin
62
- host, port = deserialize_service_instance(node.first)
63
- rescue
64
- log.error "synapse: invalid data in ZK node #{name} at #{@discovery['path']}"
62
+ host, port, name = deserialize_service_instance(node.first)
63
+ rescue StandardError => e
64
+ log.error "synapse: invalid data in ZK node #{id} at #{@discovery['path']}: #{e}"
65
65
  else
66
66
  server_port = @server_port_override ? @server_port_override : port
67
67
 
68
+ # find the numberic id in the node name; used for leader elections if enabled
69
+ numeric_id = id.split('_').last
70
+ numeric_id = NUMBERS_RE =~ numeric_id ? numeric_id.to_i : nil
71
+
68
72
  log.debug "synapse: discovered backend #{name} at #{host}:#{server_port} for service #{@name}"
69
- new_backends << { 'name' => name, 'host' => host, 'port' => server_port}
73
+ new_backends << { 'name' => name, 'host' => host, 'port' => server_port, 'id' => numeric_id}
70
74
  end
71
75
  end
72
76
  rescue ZK::Exceptions::NoNode
@@ -108,28 +112,16 @@ module Synapse
108
112
  end
109
113
  end
110
114
 
111
- # tries to extract host/port from a json hash
112
- def parse_json(data)
113
- begin
114
- json = JSON.parse data
115
- rescue Object => o
116
- return false
117
- end
118
- raise 'instance json data does not have host key' unless json.has_key?('host')
119
- raise 'instance json data does not have port key' unless json.has_key?('port')
120
- return json['host'], json['port']
121
- end
122
-
123
115
  # decode the data at a zookeeper endpoint
124
116
  def deserialize_service_instance(data)
125
117
  log.debug "synapse: deserializing process data"
118
+ decoded = JSON.parse(data)
126
119
 
127
- # if that does not work, try json
128
- host, port = parse_json(data)
129
- return host, port if host
120
+ host = decoded['host'] || (raise ValueError, 'instance json data does not have host key')
121
+ port = decoded['port'] || (raise ValueError, 'instance json data does not have port key')
122
+ name = decoded['name'] || nil
130
123
 
131
- # if we got this far, then we have a problem
132
- raise "could not decode this data:\n#{data}"
124
+ return host, port, name
133
125
  end
134
126
  end
135
127
  end
@@ -1,3 +1,3 @@
1
1
  module Synapse
2
- VERSION = "0.8.0"
2
+ VERSION = "0.9.1"
3
3
  end
@@ -6,8 +6,7 @@ describe Synapse::Haproxy do
6
6
  subject { Synapse::Haproxy.new(config['haproxy']) }
7
7
 
8
8
  it 'updating the config' do
9
- mockWatcher = mock(Synapse::ServiceWatcher)
10
- binding.pry
9
+ mockWatcher = double(Synapse::ServiceWatcher)
11
10
  subject.should_receive(:generate_config)
12
11
  subject.update_config([mockWatcher])
13
12
  end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ class Synapse::BaseWatcher
4
+ attr_reader :should_exit, :default_servers
5
+ end
6
+
7
+ describe Synapse::BaseWatcher do
8
+ let(:mocksynapse) { double() }
9
+ subject { Synapse::BaseWatcher.new(args, mocksynapse) }
10
+ let(:testargs) { { 'name' => 'foo', 'discovery' => { 'method' => 'base' }, 'haproxy' => {} }}
11
+
12
+ def remove_arg(name)
13
+ args = testargs.clone
14
+ args.delete name
15
+ args
16
+ end
17
+
18
+ context "can construct normally" do
19
+ let(:args) { testargs }
20
+ it('can at least construct') { expect { subject }.not_to raise_error }
21
+ end
22
+
23
+ ['name', 'discovery', 'haproxy'].each do |to_remove|
24
+ context "without #{to_remove} argument" do
25
+ let(:args) { remove_arg to_remove }
26
+ it('gots bang') { expect { subject }.to raise_error(ArgumentError, "missing required option #{to_remove}") }
27
+ end
28
+ end
29
+
30
+ context "normal tests" do
31
+ let(:args) { testargs }
32
+ it('is running') { expect(subject.should_exit).to equal(false) }
33
+ it('can ping') { expect(subject.ping?).to equal(true) }
34
+ it('can be stopped') do
35
+ subject.stop
36
+ expect(subject.should_exit).to equal(true)
37
+ end
38
+ end
39
+
40
+ context "with default_servers" do
41
+ default_servers = ['server1', 'server2']
42
+ let(:args) { testargs.merge({'default_servers' => default_servers}) }
43
+ it('sets default backends to default_servers') { expect(subject.backends).to equal(default_servers) }
44
+ end
45
+ end
46
+
@@ -0,0 +1,152 @@
1
+ require 'spec_helper'
2
+
3
+ class Synapse::DockerWatcher
4
+ attr_reader :check_interval, :watcher, :synapse
5
+ attr_accessor :default_servers
6
+ end
7
+
8
+ describe Synapse::DockerWatcher do
9
+ let(:mocksynapse) { double() }
10
+ subject { Synapse::DockerWatcher.new(testargs, mocksynapse) }
11
+ let(:testargs) { { 'name' => 'foo', 'discovery' => { 'method' => 'docker', 'servers' => [{'host' => 'server1.local', 'name' => 'mainserver'}], 'image_name' => 'mycool/image', 'container_port' => 6379 }, 'haproxy' => {} }}
12
+ before(:each) do
13
+ allow(subject.log).to receive(:warn)
14
+ allow(subject.log).to receive(:info)
15
+ end
16
+
17
+ def add_arg(name, value)
18
+ args = testargs.clone
19
+ args['discovery'][name] = value
20
+ args
21
+ end
22
+
23
+ context "can construct normally" do
24
+ it('can at least construct') { expect { subject }.not_to raise_error }
25
+ end
26
+
27
+ context "normal tests" do
28
+ it('starts a watcher thread') do
29
+ watcher_mock = double()
30
+ expect(Thread).to receive(:new).and_return(watcher_mock)
31
+ subject.start
32
+ expect(subject.watcher).to equal(watcher_mock)
33
+ end
34
+ it('sets default check interval') do
35
+ expect(Thread).to receive(:new).and_return(double)
36
+ subject.start
37
+ expect(subject.check_interval).to eq(15.0)
38
+ end
39
+ end
40
+
41
+ context "watch tests" do
42
+ before(:each) do
43
+ expect(subject).to receive(:sleep_until_next_check) do |arg|
44
+ subject.instance_variable_set('@should_exit', true)
45
+ end
46
+ end
47
+ it('has a happy first run path, configuring backends') do
48
+ expect(subject).to receive(:containers).and_return(['container1'])
49
+ expect(subject).to receive(:configure_backends).with(['container1'])
50
+ subject.send(:watch)
51
+ end
52
+ it('does not call configure_backends if there is no change') do
53
+ expect(subject).to receive(:containers).and_return([])
54
+ expect(subject).to_not receive(:configure_backends)
55
+ subject.send(:watch)
56
+ end
57
+ end
58
+ context "watch eats exceptions" do
59
+ it "blows up when finding containers" do
60
+ expect(subject).to receive(:containers) do |arg|
61
+ subject.instance_variable_set('@should_exit', true)
62
+ raise('throw exception inside watch')
63
+ end
64
+ expect { subject.send(:watch) }.not_to raise_error
65
+ end
66
+ end
67
+
68
+ context "configure_backends tests" do
69
+ before(:each) do
70
+ expect(subject.synapse).to receive(:'reconfigure!').at_least(:once)
71
+ end
72
+ it 'runs' do
73
+ expect { subject.send(:configure_backends, []) }.not_to raise_error
74
+ end
75
+ it 'sets backends right' do
76
+ subject.send(:configure_backends, ['foo'])
77
+ expect(subject.backends).to eq(['foo'])
78
+ end
79
+ it 'resets to default backends if no container found' do
80
+ subject.default_servers = ['fallback1']
81
+ subject.send(:configure_backends, ['foo'])
82
+ expect(subject.backends).to eq(['foo'])
83
+ subject.send(:configure_backends, [])
84
+ expect(subject.backends).to eq(['fallback1'])
85
+ end
86
+ it 'does not reset to default backends if there are no default backends' do
87
+ subject.default_servers = []
88
+ subject.send(:configure_backends, ['foo'])
89
+ expect(subject.backends).to eq(['foo'])
90
+ subject.send(:configure_backends, [])
91
+ expect(subject.backends).to eq(['foo'])
92
+ end
93
+ end
94
+
95
+ context "rewrite_container_ports tests" do
96
+ it 'doesnt break if Ports => nil' do
97
+ subject.send(:rewrite_container_ports, nil)
98
+ end
99
+ it 'works for old style port mappings' do
100
+ expect(subject.send(:rewrite_container_ports, "0.0.0.0:49153->6379/tcp, 0.0.0.0:49154->6390/tcp")).to \
101
+ eql({'6379' => '49153', '6390' => '49154'})
102
+ end
103
+ it 'works for new style port mappings' do
104
+ expect(subject.send(:rewrite_container_ports, [{'PrivatePort' => 6379, 'PublicPort' => 49153}, {'PublicPort' => 49154, 'PrivatePort' => 6390}])).to \
105
+ eql({'6379' => '49153', '6390' => '49154'})
106
+ end
107
+ end
108
+
109
+ context "container discovery tests" do
110
+ before(:each) do
111
+ getter = double()
112
+ expect(getter).to receive(:get)
113
+ expect(Docker).to receive(:connection).and_return(getter)
114
+ end
115
+
116
+ it('has a sane uri') { subject.send(:containers); expect(Docker.url).to eql('http://server1.local:4243') }
117
+
118
+ context 'old style port mappings' do
119
+ context 'works for one container' do
120
+ let(:docker_data) { [{"Ports" => "0.0.0.0:49153->6379/tcp, 0.0.0.0:49154->6390/tcp", "Image" => "mycool/image:tagname"}] }
121
+ it do
122
+ expect(Docker::Util).to receive(:parse_json).and_return(docker_data)
123
+ expect(subject.send(:containers)).to eql([{"name"=>"mainserver", "host"=>"server1.local", "port"=>"49153"}])
124
+ end
125
+ end
126
+ context 'works for multiple containers' do
127
+ let(:docker_data) { [{"Ports" => "0.0.0.0:49153->6379/tcp, 0.0.0.0:49154->6390/tcp", "Image" => "mycool/image:tagname"}, {"Ports" => "0.0.0.0:49155->6379/tcp", "Image" => "mycool/image:tagname"}] }
128
+ it do
129
+ expect(Docker::Util).to receive(:parse_json).and_return(docker_data)
130
+ expect(subject.send(:containers)).to eql([{"name"=>"mainserver", "host"=>"server1.local", "port"=>"49153"},{"name"=>"mainserver", "host"=>"server1.local", "port"=>"49155"}])
131
+ end
132
+ end
133
+ end
134
+
135
+ context 'new style port mappings' do
136
+ let(:docker_data) { [{"Ports" => [{'PrivatePort' => 6379, 'PublicPort' => 49153}, {'PublicPort' => 49154, 'PrivatePort' => 6390}], "Image" => "mycool/image:tagname"}] }
137
+ it do
138
+ expect(Docker::Util).to receive(:parse_json).and_return(docker_data)
139
+ expect(subject.send(:containers)).to eql([{"name"=>"mainserver", "host"=>"server1.local", "port"=>"49153"}])
140
+ end
141
+ end
142
+
143
+ context 'filters out wrong images' do
144
+ let(:docker_data) { [{"Ports" => "0.0.0.0:49153->6379/tcp, 0.0.0.0:49154->6390/tcp", "Image" => "mycool/image:tagname"}, {"Ports" => "0.0.0.0:49155->6379/tcp", "Image" => "wrong/image:tagname"}] }
145
+ it do
146
+ expect(Docker::Util).to receive(:parse_json).and_return(docker_data)
147
+ expect(subject.send(:containers)).to eql([{"name"=>"mainserver", "host"=>"server1.local", "port"=>"49153"}])
148
+ end
149
+ end
150
+ end
151
+ end
152
+
data/spec/spec_helper.rb CHANGED
@@ -4,7 +4,7 @@
4
4
  # loaded once.
5
5
  #
6
6
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
- require '../lib/synapse'
7
+ require "#{File.dirname(__FILE__)}/../lib/synapse"
8
8
  require 'pry'
9
9
  require 'support/config'
10
10
 
data/synapse.gemspec CHANGED
@@ -19,6 +19,7 @@ Gem::Specification.new do |gem|
19
19
  gem.add_runtime_dependency "zk", "~> 1.9.2"
20
20
  gem.add_runtime_dependency "docker-api", "~> 1.7.2"
21
21
 
22
+ gem.add_development_dependency "rake"
22
23
  gem.add_development_dependency "rspec"
23
24
  gem.add_development_dependency "pry"
24
25
  gem.add_development_dependency "pry-nav"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: synapse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-01-27 00:00:00.000000000 Z
12
+ date: 2014-02-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: zk
@@ -43,6 +43,22 @@ dependencies:
43
43
  - - ~>
44
44
  - !ruby/object:Gem::Version
45
45
  version: 1.7.2
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
46
62
  - !ruby/object:Gem::Dependency
47
63
  name: rspec
48
64
  requirement: !ruby/object:Gem::Requirement
@@ -102,6 +118,7 @@ files:
102
118
  - .gitignore
103
119
  - .mailmap
104
120
  - .rspec
121
+ - .travis.yml
105
122
  - Gemfile
106
123
  - Gemfile.lock
107
124
  - LICENSE.txt
@@ -126,6 +143,8 @@ files:
126
143
  - lib/synapse/service_watcher/zookeeper.rb
127
144
  - lib/synapse/version.rb
128
145
  - spec/lib/synapse/haproxy_spec.rb
146
+ - spec/lib/synapse/service_watcher_base_spec.rb
147
+ - spec/lib/synapse/service_watcher_docker_spec.rb
129
148
  - spec/spec_helper.rb
130
149
  - spec/support/config.rb
131
150
  - spec/support/minimum.conf.yaml
@@ -142,12 +161,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
142
161
  - - ! '>='
143
162
  - !ruby/object:Gem::Version
144
163
  version: '0'
164
+ segments:
165
+ - 0
166
+ hash: 575568743231626432
145
167
  required_rubygems_version: !ruby/object:Gem::Requirement
146
168
  none: false
147
169
  requirements:
148
170
  - - ! '>='
149
171
  - !ruby/object:Gem::Version
150
172
  version: '0'
173
+ segments:
174
+ - 0
175
+ hash: 575568743231626432
151
176
  requirements: []
152
177
  rubyforge_project:
153
178
  rubygems_version: 1.8.23
@@ -156,6 +181,8 @@ specification_version: 3
156
181
  summary: ': Write a gem summary'
157
182
  test_files:
158
183
  - spec/lib/synapse/haproxy_spec.rb
184
+ - spec/lib/synapse/service_watcher_base_spec.rb
185
+ - spec/lib/synapse/service_watcher_docker_spec.rb
159
186
  - spec/spec_helper.rb
160
187
  - spec/support/config.rb
161
188
  - spec/support/minimum.conf.yaml