synapse 0.13.8 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,12 +1,13 @@
1
1
  ## Watcher Classes
2
2
 
3
3
  Watchers are the piece of Synapse that watch an external service registry
4
- and reflect those changes in the local HAProxy state. Watchers should conform
5
- to the interface specified by `BaseWatcher` and when your watcher has received
6
- an update from the service registry you should call
4
+ and reflect those changes in the local configuration state. Watchers should
5
+ conform to the interface specified by `BaseWatcher` and when your watcher has
6
+ received an update from the service registry you should call
7
7
  `set_backends(new_backends)` to trigger a sync of your watcher state with local
8
- HAProxy state. See the [`Backend Interface`](#backend_interface) section for
9
- what service registrations Synapse understands.
8
+ configuration (HAProxy, files, etc ...) state. See the
9
+ [`Backend Interface`](#backend_interface) section for what fields in
10
+ registrations Synapse understands.
10
11
 
11
12
  ```ruby
12
13
  require "synapse/service_watcher/base"
@@ -7,7 +7,7 @@ class Synapse::ServiceWatcher
7
7
 
8
8
  LEADER_WARN_INTERVAL = 30
9
9
 
10
- attr_reader :name, :haproxy
10
+ attr_reader :name, :config_for_generator
11
11
 
12
12
  def initialize(opts={}, synapse)
13
13
  super()
@@ -15,7 +15,7 @@ class Synapse::ServiceWatcher
15
15
  @synapse = synapse
16
16
 
17
17
  # set required service parameters
18
- %w{name discovery haproxy}.each do |req|
18
+ %w{name discovery}.each do |req|
19
19
  raise ArgumentError, "missing required option #{req}" unless opts[req]
20
20
  end
21
21
 
@@ -33,17 +33,15 @@ class Synapse::ServiceWatcher
33
33
  @leader_election = opts['leader_election'] || false
34
34
  @leader_last_warn = Time.now - LEADER_WARN_INTERVAL
35
35
 
36
- # the haproxy config
37
- @haproxy = opts['haproxy']
38
- @haproxy['server_options'] ||= ""
39
- @haproxy['server_port_override'] ||= nil
40
- %w{backend frontend listen}.each do |sec|
41
- @haproxy[sec] ||= []
42
- end
43
-
44
- unless @haproxy.include?('port')
45
- 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`"
46
- end
36
+ @config_for_generator = Hash[
37
+ @synapse.available_generators.collect do |generator_name, generator|
38
+ watcher_provided_config = opts[generator_name] || {}
39
+ normalized_generator_opts = generator.normalize_watcher_provided_config(
40
+ @name, watcher_provided_config
41
+ )
42
+ [generator_name, normalized_generator_opts]
43
+ end
44
+ ]
47
45
 
48
46
  # set initial backends to default servers, if any
49
47
  @default_servers = opts['default_servers'] || []
@@ -55,6 +53,22 @@ class Synapse::ServiceWatcher
55
53
  # use the previous backends that we already know about.
56
54
  @use_previous_backends = opts.fetch('use_previous_backends', true)
57
55
 
56
+ # If backends discovered with this watcher lack ports or discovered
57
+ # ports should be ignored, this option indicates that
58
+ @backend_port_override = opts['backend_port_override'] || nil
59
+
60
+ # For backwards compatability we support server_port_override
61
+ # This will be removed in future versions
62
+ if @backend_port_override.nil? && @config_for_generator['haproxy']
63
+ @backend_port_override = @config_for_generator['haproxy']['server_port_override']
64
+ end
65
+
66
+ unless @backend_port_override.nil?
67
+ unless @backend_port_override.to_s.match(/^\d+$/)
68
+ raise ArgumentError, "Invalid backend_port_override value"
69
+ end
70
+ end
71
+
58
72
  # set a flag used to tell the watchers to exit
59
73
  # this is not used in every watcher
60
74
  @should_exit = false
@@ -62,6 +76,11 @@ class Synapse::ServiceWatcher
62
76
  validate_discovery_opts
63
77
  end
64
78
 
79
+ def haproxy
80
+ log.warn "synapse: service watcher #{@name} accessing watcher.haproxy. This is DEPRECATED and will be removed in future iterations, use watcher.config_for_generator['haproxy'] instead."
81
+ config_for_generator['haproxy']
82
+ end
83
+
65
84
  # this should be overridden to actually start your watcher
66
85
  def start
67
86
  log.info "synapse: starting stub watcher; this means doing nothing at all!"
@@ -135,6 +154,12 @@ class Synapse::ServiceWatcher
135
154
  def set_backends(new_backends)
136
155
  # Aggregate and deduplicate all potential backend service instances.
137
156
  new_backends = (new_backends + @default_servers) if @keep_default_servers
157
+ # Substitute backend_port_override for the provided port
158
+ new_backends = new_backends.collect do |b|
159
+ port = @backend_port_override ? @backend_port_override : b['port']
160
+ b.merge({'port' => port})
161
+ end
162
+
138
163
  new_backends = new_backends.uniq {|b|
139
164
  [b['host'], b['port'], b.fetch('name', '')]
140
165
  }
@@ -36,13 +36,9 @@ class Synapse::ServiceWatcher
36
36
 
37
37
  # As we're only looking up instances with hostnames/IPs, need to
38
38
  # be explicitly told which port the service we're balancing for listens on.
39
- unless @haproxy['server_port_override']
39
+ unless @backend_port_override
40
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'].to_s.match(/^\d+$/)
45
- raise ArgumentError, "Invalid server_port_override value"
41
+ "Missing backend_port_override for service #{@name} - which port are backends listening on?"
46
42
  end
47
43
 
48
44
  # aws region is optional in the SDK, aws will use a default value if not provided
@@ -93,7 +89,6 @@ class Synapse::ServiceWatcher
93
89
  new_backends << {
94
90
  'name' => instance.private_dns_name,
95
91
  'host' => instance.private_ip_address,
96
- 'port' => @haproxy['server_port_override'],
97
92
  }
98
93
  end
99
94
 
@@ -143,15 +143,13 @@ class Synapse::ServiceWatcher
143
143
  rescue StandardError => e
144
144
  log.error "synapse: invalid data in ZK node #{id} at #{@discovery['path']}: #{e}"
145
145
  else
146
- server_port = @haproxy['server_port_override'] ? @haproxy['server_port_override'] : port
147
-
148
146
  # find the numberic id in the node name; used for leader elections if enabled
149
147
  numeric_id = id.split('_').last
150
148
  numeric_id = NUMBERS_RE =~ numeric_id ? numeric_id.to_i : nil
151
149
 
152
- log.debug "synapse: discovered backend #{name} at #{host}:#{server_port} for service #{@name}"
150
+ log.debug "synapse: discovered backend #{name} at #{host}:#{port} for service #{@name}"
153
151
  new_backends << {
154
- 'name' => name, 'host' => host, 'port' => server_port,
152
+ 'name' => name, 'host' => host, 'port' => port,
155
153
  'id' => numeric_id, 'weight' => weight,
156
154
  'haproxy_server_options' => haproxy_server_options,
157
155
  'labels' => labels
@@ -218,7 +218,6 @@ class Synapse::ServiceWatcher
218
218
  def mk_child_watcher_opts(discovery_opts)
219
219
  {
220
220
  'name' => @name,
221
- 'haproxy' => @haproxy,
222
221
  'discovery' => discovery_opts,
223
222
  'default_servers' => @default_servers,
224
223
  }
@@ -1,3 +1,3 @@
1
1
  module Synapse
2
- VERSION = "0.13.8"
2
+ VERSION = "0.14.0"
3
3
  end
@@ -1,8 +1,9 @@
1
1
  require 'spec_helper'
2
2
  require 'fileutils'
3
+ require 'synapse/config_generator/file_output'
3
4
 
4
- describe Synapse::FileOutput do
5
- subject { Synapse::FileOutput.new(config['file_output']) }
5
+ describe Synapse::ConfigGenerator::FileOutput do
6
+ subject { Synapse::ConfigGenerator::FileOutput.new(config['file_output']) }
6
7
 
7
8
  before(:example) do
8
9
  FileUtils.mkdir_p(config['file_output']['output_directory'])
@@ -1,16 +1,19 @@
1
1
  require 'spec_helper'
2
+ require 'synapse/config_generator/haproxy'
2
3
 
3
4
  class MockWatcher; end;
4
5
 
5
- describe Synapse::Haproxy do
6
- subject { Synapse::Haproxy.new(config['haproxy']) }
6
+ describe Synapse::ConfigGenerator::Haproxy do
7
+ subject { Synapse::ConfigGenerator::Haproxy.new(config['haproxy']) }
7
8
 
8
9
  let(:mockwatcher) do
9
10
  mockWatcher = double(Synapse::ServiceWatcher)
10
11
  allow(mockWatcher).to receive(:name).and_return('example_service')
11
12
  backends = [{ 'host' => 'somehost', 'port' => 5555}]
12
13
  allow(mockWatcher).to receive(:backends).and_return(backends)
13
- allow(mockWatcher).to receive(:haproxy).and_return({'server_options' => "check inter 2000 rise 3 fall 2"})
14
+ allow(mockWatcher).to receive(:config_for_generator).and_return({
15
+ 'haproxy' => {'server_options' => "check inter 2000 rise 3 fall 2"}
16
+ })
14
17
  mockWatcher
15
18
  end
16
19
 
@@ -19,7 +22,9 @@ describe Synapse::Haproxy do
19
22
  allow(mockWatcher).to receive(:name).and_return('example_service2')
20
23
  backends = [{ 'host' => 'somehost', 'port' => 5555, 'haproxy_server_options' => 'backup'}]
21
24
  allow(mockWatcher).to receive(:backends).and_return(backends)
22
- allow(mockWatcher).to receive(:haproxy).and_return({'server_options' => "check inter 2000 rise 3 fall 2"})
25
+ allow(mockWatcher).to receive(:config_for_generator).and_return({
26
+ 'haproxy' => {'server_options' => "check inter 2000 rise 3 fall 2"}
27
+ })
23
28
  mockWatcher
24
29
  end
25
30
 
@@ -28,30 +33,174 @@ describe Synapse::Haproxy do
28
33
  allow(mockWatcher).to receive(:name).and_return('example_service3')
29
34
  backends = [{ 'host' => 'somehost', 'port' => 5555}]
30
35
  allow(mockWatcher).to receive(:backends).and_return(backends)
31
- allow(mockWatcher).to receive(:haproxy).and_return({'server_options' => "check inter 2000 rise 3 fall 2", 'cookie_value_method' => 'hash'})
36
+ allow(mockWatcher).to receive(:config_for_generator).and_return({
37
+ 'haproxy' => {'server_options' => "check inter 2000 rise 3 fall 2", 'cookie_value_method' => 'hash'}
38
+ })
32
39
  mockWatcher
33
40
  end
34
41
 
35
42
  let(:mockwatcher_frontend) do
36
43
  mockWatcher = double(Synapse::ServiceWatcher)
37
44
  allow(mockWatcher).to receive(:name).and_return('example_service4')
38
- allow(mockWatcher).to receive(:haproxy).and_return('port' => 2200)
45
+ allow(mockWatcher).to receive(:config_for_generator).and_return({
46
+ 'haproxy' => {'port' => 2200}
47
+ })
48
+ mockWatcher
49
+ end
50
+
51
+ let(:mockwatcher_frontend_with_bind_options) do
52
+ mockWatcher = double(Synapse::ServiceWatcher)
53
+ allow(mockWatcher).to receive(:name).and_return('example_service4')
54
+ allow(mockWatcher).to receive(:config_for_generator).and_return({
55
+ 'haproxy' => {
56
+ 'port' => 2200,
57
+ 'bind_options' => 'ssl no-sslv3 crt /path/to/cert/example.pem ciphers ECDHE-ECDSA-CHACHA20-POLY1305'
58
+ }
59
+ })
39
60
  mockWatcher
40
61
  end
41
62
 
42
63
  let(:mockwatcher_frontend_with_bind_address) do
43
64
  mockWatcher = double(Synapse::ServiceWatcher)
44
65
  allow(mockWatcher).to receive(:name).and_return('example_service5')
45
- allow(mockWatcher).to receive(:haproxy).and_return('port' => 2200, 'bind_address' => "127.0.0.3")
66
+ allow(mockWatcher).to receive(:config_for_generator).and_return({
67
+ 'haproxy' => {'port' => 2200, 'bind_address' => "127.0.0.3"}
68
+ })
46
69
  mockWatcher
47
70
  end
48
71
 
72
+ let(:mockwatcher_disabled) do
73
+ mockWatcher = double(Synapse::ServiceWatcher)
74
+ allow(mockWatcher).to receive(:name).and_return('disabled_watcher')
75
+ backends = [{ 'host' => 'somehost', 'port' => 5555}]
76
+ allow(mockWatcher).to receive(:backends).and_return(backends)
77
+ allow(mockWatcher).to receive(:config_for_generator).and_return({
78
+ 'haproxy' => {'port' => 2200, 'disabled' => true}
79
+ })
80
+ mockWatcher
81
+ end
82
+
83
+ describe '#initialize' do
84
+ it 'succeeds on minimal config' do
85
+ conf = {
86
+ 'global' => [],
87
+ 'defaults' => [],
88
+ 'do_writes' => false,
89
+ 'do_reloads' => false,
90
+ 'do_socket' => false
91
+ }
92
+ Synapse::ConfigGenerator::Haproxy.new(conf)
93
+ expect{Synapse::ConfigGenerator::Haproxy.new(conf)}.not_to raise_error
94
+ end
95
+
96
+ it 'validates req_pairs' do
97
+ req_pairs = {
98
+ 'do_writes' => 'config_file_path',
99
+ 'do_socket' => 'socket_file_path',
100
+ 'do_reloads' => 'reload_command'
101
+ }
102
+ valid_conf = {
103
+ 'global' => [],
104
+ 'defaults' => [],
105
+ 'do_reloads' => false,
106
+ 'do_socket' => false,
107
+ 'do_writes' => false
108
+ }
109
+
110
+ req_pairs.each do |key, value|
111
+ conf = valid_conf.clone
112
+ conf[key] = true
113
+ expect{Synapse::ConfigGenerator::Haproxy.new(conf)}.
114
+ to raise_error(ArgumentError, "the `#{value}` option is required when `#{key}` is true")
115
+ end
116
+
117
+ end
118
+
119
+ it 'properly defaults do_writes, do_socket, do_reloads' do
120
+ conf = {
121
+ 'global' => [],
122
+ 'defaults' => [],
123
+ 'config_file_path' => 'test_file',
124
+ 'socket_file_path' => 'test_socket',
125
+ 'reload_command' => 'test_reload'
126
+ }
127
+
128
+ expect{Synapse::ConfigGenerator::Haproxy.new(conf)}.not_to raise_error
129
+ haproxy = Synapse::ConfigGenerator::Haproxy.new(conf)
130
+ expect(haproxy.opts['do_writes']).to eql(true)
131
+ expect(haproxy.opts['do_socket']).to eql(true)
132
+ expect(haproxy.opts['do_reloads']).to eql(true)
133
+ end
134
+
135
+ it 'complains when req_pairs are not passed at all' do
136
+ conf = {
137
+ 'global' => [],
138
+ 'defaults' => [],
139
+ }
140
+ expect{Synapse::ConfigGenerator::Haproxy.new(conf)}.to raise_error(ArgumentError)
141
+ end
142
+ end
143
+
49
144
  describe '#name' do
50
145
  it 'returns haproxy' do
51
146
  expect(subject.name).to eq('haproxy')
52
147
  end
53
148
  end
54
149
 
150
+ describe 'disabled watcher' do
151
+ let(:watchers) { [mockwatcher, mockwatcher_disabled] }
152
+ let(:socket_file_path) { 'socket_file_path' }
153
+
154
+ before do
155
+ config['haproxy']['do_socket'] = true
156
+ config['haproxy']['socket_file_path'] = socket_file_path
157
+ end
158
+
159
+ it 'does not generate config' do
160
+ allow(subject).to receive(:parse_watcher_config).and_return({})
161
+ expect(subject).to receive(:generate_frontend_stanza).exactly(:once).with(mockwatcher, nil)
162
+ expect(subject).to receive(:generate_backend_stanza).exactly(:once).with(mockwatcher, nil)
163
+ subject.update_config(watchers)
164
+ end
165
+
166
+ context 'when configuration via the socket succeeds' do
167
+ before do
168
+ subject.instance_variable_set(:@restart_required, false)
169
+ allow(subject).to receive(:generate_config).exactly(:once).and_return 'mock_config'
170
+ end
171
+
172
+ it 'does not cause a restart due to the socket' do
173
+ mock_socket_output = "example_service,somehost:5555"
174
+ allow(subject).to receive(:talk_to_socket).with(socket_file_path, "show stat\n").and_return mock_socket_output
175
+
176
+ expect(subject).to receive(:talk_to_socket).exactly(:once).with(
177
+ socket_file_path, "enable server example_service/somehost:5555\n"
178
+ ).and_return "\n"
179
+
180
+ subject.update_config(watchers)
181
+
182
+ expect(subject.instance_variable_get(:@restart_required)).to eq false
183
+ end
184
+
185
+ it 'disables existing servers on the socket' do
186
+ mock_socket_output = "example_service,somehost:5555\ndisabled_watcher,somehost:5555"
187
+ allow(subject).to receive(:talk_to_socket).with(socket_file_path, "show stat\n").and_return mock_socket_output
188
+
189
+
190
+ expect(subject).to receive(:talk_to_socket).exactly(:once).with(
191
+ socket_file_path, "enable server example_service/somehost:5555\n"
192
+ ).and_return "\n"
193
+ expect(subject).to receive(:talk_to_socket).exactly(:once).with(
194
+ socket_file_path, "disable server disabled_watcher/somehost:5555\n"
195
+ ).and_return "\n"
196
+
197
+ subject.update_config(watchers)
198
+
199
+ expect(subject.instance_variable_get(:@restart_required)).to eq false
200
+ end
201
+ end
202
+ end
203
+
55
204
  describe '#update_config' do
56
205
  let(:watchers) { [mockwatcher_frontend, mockwatcher_frontend_with_bind_address] }
57
206
 
@@ -254,7 +403,12 @@ describe Synapse::Haproxy do
254
403
  context "when #{order_option} is specified for backend_order" do
255
404
  it 'generates backend stanza in correct order' do
256
405
  mockConfig = []
257
- allow(mockwatcher_with_multiple_backends).to receive(:haproxy).and_return({'server_options' => "check inter 2000 rise 3 fall 2", 'backend_order' => order_option})
406
+ allow(mockwatcher_with_multiple_backends).to receive(:config_for_generator).and_return({
407
+ 'haproxy' => {
408
+ 'server_options' => "check inter 2000 rise 3 fall 2",
409
+ 'backend_order' => order_option
410
+ }
411
+ })
258
412
  expect(subject.generate_backend_stanza(mockwatcher_with_multiple_backends, mockConfig)).to eql(multiple_backends_stanza_map[order_option])
259
413
  end
260
414
  end
@@ -281,6 +435,11 @@ describe Synapse::Haproxy do
281
435
  expect(subject.generate_frontend_stanza(mockwatcher_frontend, mockConfig)).to eql(["\nfrontend example_service4", [], "\tbind localhost:2200", "\tdefault_backend example_service4"])
282
436
  end
283
437
 
438
+ it 'generates frontend stanza with bind options ' do
439
+ mockConfig = []
440
+ expect(subject.generate_frontend_stanza(mockwatcher_frontend_with_bind_options, mockConfig)).to eql(["\nfrontend example_service4", [], "\tbind localhost:2200 ssl no-sslv3 crt /path/to/cert/example.pem ciphers ECDHE-ECDSA-CHACHA20-POLY1305", "\tdefault_backend example_service4"])
441
+ end
442
+
284
443
  it 'respects frontend bind_address ' do
285
444
  mockConfig = []
286
445
  expect(subject.generate_frontend_stanza(mockwatcher_frontend_with_bind_address, mockConfig)).to eql(["\nfrontend example_service5", [], "\tbind 127.0.0.3:2200", "\tdefault_backend example_service5"])
@@ -5,7 +5,14 @@ class Synapse::ServiceWatcher::BaseWatcher
5
5
  end
6
6
 
7
7
  describe Synapse::ServiceWatcher::BaseWatcher do
8
- let(:mocksynapse) { double() }
8
+ let(:mocksynapse) do
9
+ mock_synapse = instance_double(Synapse::Synapse)
10
+ mockgenerator = Synapse::ConfigGenerator::BaseGenerator.new()
11
+ allow(mock_synapse).to receive(:available_generators).and_return({
12
+ 'haproxy' => mockgenerator
13
+ })
14
+ mock_synapse
15
+ end
9
16
  subject { Synapse::ServiceWatcher::BaseWatcher.new(args, mocksynapse) }
10
17
  let(:testargs) { { 'name' => 'foo', 'discovery' => { 'method' => 'base' }, 'haproxy' => {} }}
11
18
 
@@ -20,7 +27,7 @@ describe Synapse::ServiceWatcher::BaseWatcher do
20
27
  it('can at least construct') { expect { subject }.not_to raise_error }
21
28
  end
22
29
 
23
- ['name', 'discovery', 'haproxy'].each do |to_remove|
30
+ ['name', 'discovery'].each do |to_remove|
24
31
  context "without #{to_remove} argument" do
25
32
  let(:args) { remove_arg to_remove }
26
33
  it('gots bang') { expect { subject }.to raise_error(ArgumentError, "missing required option #{to_remove}") }
@@ -7,7 +7,14 @@ class Synapse::ServiceWatcher::DockerWatcher
7
7
  end
8
8
 
9
9
  describe Synapse::ServiceWatcher::DockerWatcher do
10
- let(:mocksynapse) { double() }
10
+ let(:mocksynapse) do
11
+ mock_synapse = instance_double(Synapse::Synapse)
12
+ mockgenerator = Synapse::ConfigGenerator::BaseGenerator.new()
13
+ allow(mock_synapse).to receive(:available_generators).and_return({
14
+ 'haproxy' => mockgenerator
15
+ })
16
+ mock_synapse
17
+ end
11
18
  subject { Synapse::ServiceWatcher::DockerWatcher.new(testargs, mocksynapse) }
12
19
  let(:testargs) { { 'name' => 'foo', 'discovery' => { 'method' => 'docker', 'servers' => [{'host' => 'server1.local', 'name' => 'mainserver'}], 'image_name' => 'mycool/image', 'container_port' => 6379 }, 'haproxy' => {} }}
13
20
  before(:each) do