invoker 1.4.1 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/.rubocop.yml +0 -1
  4. data/.travis.yml +4 -2
  5. data/Dockerfile +7 -0
  6. data/Gemfile +3 -0
  7. data/Rakefile +8 -0
  8. data/examples/hello_sinatra.rb +26 -0
  9. data/examples/sample.ini +3 -0
  10. data/invoker.gemspec +2 -1
  11. data/lib/invoker.rb +20 -3
  12. data/lib/invoker/cli.rb +32 -6
  13. data/lib/invoker/dns_cache.rb +2 -2
  14. data/lib/invoker/ipc/add_http_command.rb +1 -1
  15. data/lib/invoker/ipc/dns_check_command.rb +2 -1
  16. data/lib/invoker/ipc/message.rb +2 -2
  17. data/lib/invoker/parsers/config.rb +4 -1
  18. data/lib/invoker/power/balancer.rb +16 -4
  19. data/lib/invoker/power/config.rb +3 -2
  20. data/lib/invoker/power/dns.rb +1 -1
  21. data/lib/invoker/power/pf_migrate.rb +1 -1
  22. data/lib/invoker/power/setup.rb +43 -4
  23. data/lib/invoker/power/setup/distro/arch.rb +1 -4
  24. data/lib/invoker/power/setup/distro/base.rb +23 -21
  25. data/lib/invoker/power/setup/distro/debian.rb +1 -1
  26. data/lib/invoker/power/setup/distro/opensuse.rb +11 -0
  27. data/lib/invoker/power/setup/distro/redhat.rb +1 -7
  28. data/lib/invoker/power/setup/files/invoker_forwarder.sh.erb +17 -0
  29. data/lib/invoker/power/setup/files/socat_invoker.service +12 -0
  30. data/lib/invoker/power/setup/linux_setup.rb +46 -29
  31. data/lib/invoker/power/setup/osx_setup.rb +16 -28
  32. data/lib/invoker/power/url_rewriter.rb +6 -3
  33. data/lib/invoker/process_manager.rb +13 -7
  34. data/lib/invoker/version.rb +1 -1
  35. data/readme.md +4 -4
  36. data/spec/invoker/commander_spec.rb +19 -8
  37. data/spec/invoker/config_spec.rb +22 -27
  38. data/spec/invoker/invoker_spec.rb +2 -1
  39. data/spec/invoker/ipc/client_handler_spec.rb +11 -1
  40. data/spec/invoker/power/config_spec.rb +2 -1
  41. data/spec/invoker/power/pf_migrate_spec.rb +7 -0
  42. data/spec/invoker/power/setup/linux_setup_spec.rb +57 -9
  43. data/spec/invoker/power/setup/osx_setup_spec.rb +22 -8
  44. data/spec/invoker/power/url_rewriter_spec.rb +33 -1
  45. data/spec/invoker/power/web_sockets_spec.rb +61 -0
  46. data/spec/invoker/process_manager_spec.rb +34 -2
  47. data/spec/spec_helper.rb +12 -16
  48. metadata +27 -35
  49. data/spec/support/mock_setup_file.rb +0 -64
@@ -134,59 +134,53 @@ command = ls
134
134
  end
135
135
  end
136
136
 
137
- describe "loading power config" do
137
+ describe "loading power config", fakefs: true do
138
138
  before do
139
- @file = Tempfile.new(["config", ".ini"])
139
+ FileUtils.mkdir_p('/tmp')
140
+ FileUtils.mkdir_p(inv_conf_dir)
141
+ File.open("/tmp/foo.ini", "w") { |fl| fl.write("") }
140
142
  end
141
143
 
142
144
  it "does not load config if platform is darwin but there is no power config file" do
143
145
  Invoker::Power::Config.expects(:load_config).never
144
- Invoker::Parsers::Config.new(@file.path, 9000)
146
+ Invoker::Parsers::Config.new("/tmp/foo.ini", 9000)
145
147
  end
146
148
 
147
149
  it "loads config if platform is darwin and power config file exists" do
148
150
  File.open(Invoker::Power::Config.config_file, "w") { |fl| fl.puts "sample" }
149
151
  Invoker::Power::Config.expects(:load_config).once
150
- Invoker::Parsers::Config.new(@file.path, 9000)
152
+ Invoker::Parsers::Config.new("/tmp/foo.ini", 9000)
151
153
  end
152
154
  end
153
155
 
154
156
  describe "Procfile" do
155
157
  it "should load Procfiles and create config object" do
156
- begin
157
- File.open("/tmp/Procfile", "w") {|fl|
158
- fl.write <<-EOD
158
+ File.open("/tmp/Procfile", "w") {|fl|
159
+ fl.write <<-EOD
159
160
  web: bundle exec rails s -p $PORT
160
161
  EOD
161
- }
162
- config = Invoker::Parsers::Config.new("/tmp/Procfile", 9000)
163
- command1 = config.processes.first
162
+ }
163
+ config = Invoker::Parsers::Config.new("/tmp/Procfile", 9000)
164
+ command1 = config.processes.first
164
165
 
165
- expect(command1.port).to eq(9000)
166
- expect(command1.cmd).to match(/bundle exec rails/)
167
- ensure
168
- File.delete("/tmp/Procfile")
169
- end
166
+ expect(command1.port).to eq(9000)
167
+ expect(command1.cmd).to match(/bundle exec rails/)
170
168
  end
171
169
  end
172
170
 
173
171
  describe "Copy of DNS information" do
174
172
  it "should allow copy of DNS information" do
175
- begin
176
- File.open("/tmp/Procfile", "w") {|fl|
177
- fl.write <<-EOD
173
+ File.open("/tmp/Procfile", "w") {|fl|
174
+ fl.write <<-EOD
178
175
  web: bundle exec rails s -p $PORT
179
176
  EOD
180
- }
181
- Invoker.load_invoker_config("/tmp/Procfile", 9000)
182
- dns_cache = Invoker::DNSCache.new(Invoker.config)
177
+ }
178
+ Invoker.load_invoker_config("/tmp/Procfile", 9000)
179
+ dns_cache = Invoker::DNSCache.new(Invoker.config)
183
180
 
184
- expect(dns_cache.dns_data).to_not be_empty
185
- expect(dns_cache.dns_data['web']).to_not be_empty
186
- expect(dns_cache.dns_data['web']['port']).to eql 9000
187
- ensure
188
- File.delete("/tmp/Procfile")
189
- end
181
+ expect(dns_cache.dns_data).to_not be_empty
182
+ expect(dns_cache.dns_data['web']).to_not be_empty
183
+ expect(dns_cache.dns_data['web']['port']).to eql 9000
190
184
  end
191
185
  end
192
186
 
@@ -227,6 +221,7 @@ command = bundle exec rails s -p $PORT
227
221
  describe "global config file" do
228
222
  it "should use global config file if available" do
229
223
  begin
224
+ FileUtils.mkdir_p(Invoker::Power::Config.config_dir)
230
225
  filename = "#{Invoker::Power::Config.config_dir}/foo.ini"
231
226
  file = File.open(filename, "w")
232
227
  config_data =<<-EOD
@@ -13,7 +13,8 @@ describe "Invoker" do
13
13
  end
14
14
  end
15
15
 
16
- describe "#can_run_balancer?" do
16
+ describe "#can_run_balancer?", fakefs: true do
17
+ before { FileUtils.mkdir_p(Invoker::Power::Config.config_dir) }
17
18
  it "should return false if setup command was not run" do
18
19
  expect(Invoker.can_run_balancer?).to be_falsey
19
20
  end
@@ -35,7 +35,17 @@ describe Invoker::IPC::ClientHandler do
35
35
  describe "add_http command" do
36
36
  let(:message_object) { MM::AddHttp.new(process_name: 'foo', port: 9000)}
37
37
  it "adds the process name and port to dns cache" do
38
- invoker_dns_cache.expects(:add).with('foo', 9000)
38
+ invoker_dns_cache.expects(:add).with('foo', 9000, nil)
39
+ client_socket.string = message_object.encoded_message
40
+
41
+ client.read_and_execute
42
+ end
43
+ end
44
+
45
+ describe "add_http command with optional ip" do
46
+ let(:message_object) { MM::AddHttp.new(process_name: 'foo', port: 9000, ip: '192.168.0.1')}
47
+ it "adds the process name, port and host ip to dns cache" do
48
+ invoker_dns_cache.expects(:add).with('foo', 9000, '192.168.0.1')
39
49
  client_socket.string = message_object.encoded_message
40
50
 
41
51
  client.read_and_execute
@@ -1,8 +1,9 @@
1
1
  require "spec_helper"
2
2
 
3
- describe "Invoker Power configuration" do
3
+ describe "Invoker Power configuration", fakefs: true do
4
4
  describe "#create" do
5
5
  it "should create a config file given a hash" do
6
+ FileUtils.mkdir_p(inv_conf_dir)
6
7
  config = Invoker::Power::Config.create(
7
8
  dns_port: 1200, http_port: 1201, ipfw_rule_number: 010
8
9
  )
@@ -2,6 +2,7 @@ require "spec_helper"
2
2
 
3
3
  describe Invoker::Power::PfMigrate do
4
4
  before do
5
+ FileUtils.mkdir_p("/tmp/.invoker")
5
6
  @old_firewall_file = Invoker::Power::OsxSetup::FIREWALL_PLIST_FILE
6
7
  Invoker::Power::OsxSetup.const_set(:FIREWALL_PLIST_FILE, "/tmp/.invoker/firewall")
7
8
  end
@@ -54,12 +55,18 @@ describe Invoker::Power::PfMigrate do
54
55
 
55
56
  describe "#migrate" do
56
57
  before do
58
+ @original_invoker_config = Invoker.config
57
59
  mock_config = mock()
58
60
  mock_config.stubs(:http_port).returns(80)
59
61
  mock_config.stubs(:https_port).returns(443)
62
+ mock_config.stubs(:tld).returns('dev')
60
63
  Invoker.config = mock_config
61
64
  end
62
65
 
66
+ after do
67
+ Invoker.config = @original_invoker_config
68
+ end
69
+
63
70
  it "should migrate firewall to new system" do
64
71
  pf_migrator.expects(:firewall_config_requires_migration?).returns(true)
65
72
  pf_migrator.expects(:ask_user_for_migration).returns(true)
@@ -1,9 +1,31 @@
1
1
  require "spec_helper"
2
2
  require "invoker/power/setup/distro/ubuntu"
3
+ require "invoker/power/setup/distro/opensuse"
3
4
 
4
- describe Invoker::Power::LinuxSetup do
5
- let(:invoker_setup) { Invoker::Power::LinuxSetup.new }
6
- let(:distro_installer) { Invoker::Power::Distro::Ubuntu.new }
5
+ def mock_socat_scripts
6
+ FakeFS.deactivate!
7
+ socat_content = File.read(invoker_setup.forwarder_script)
8
+ socat_systemd = File.read(invoker_setup.socat_unit)
9
+ FakeFS.activate!
10
+ FileUtils.mkdir_p(File.dirname(invoker_setup.forwarder_script))
11
+ FileUtils.mkdir_p(File.dirname(invoker_setup.socat_unit))
12
+ File.open(invoker_setup.socat_unit, "w") do |fl|
13
+ fl.write(socat_systemd)
14
+ end
15
+ File.open(invoker_setup.forwarder_script, "w") do |fl|
16
+ fl.write(socat_content)
17
+ end
18
+ FileUtils.mkdir_p("/usr/bin")
19
+ end
20
+
21
+ describe Invoker::Power::LinuxSetup, fakefs: true do
22
+ before do
23
+ FileUtils.mkdir_p(inv_conf_dir)
24
+ FileUtils.mkdir_p(Invoker::Power::Distro::Base::RESOLVER_DIR)
25
+ end
26
+
27
+ let(:invoker_setup) { Invoker::Power::LinuxSetup.new('dev') }
28
+ let(:distro_installer) { Invoker::Power::Distro::Ubuntu.new('dev') }
7
29
 
8
30
  describe "should only proceed after user confirmation" do
9
31
  before { invoker_setup.distro_installer = distro_installer }
@@ -27,8 +49,20 @@ describe Invoker::Power::LinuxSetup do
27
49
  end
28
50
  end
29
51
 
30
- describe "configuring dnsmasq and rinetd" do
31
- before { invoker_setup.distro_installer = distro_installer }
52
+ describe "configuring dnsmasq and socat" do
53
+ before(:all) do
54
+ @original_invoker_config = Invoker.config
55
+ Invoker.config = mock
56
+ end
57
+
58
+ after(:all) do
59
+ Invoker.config = @original_invoker_config
60
+ end
61
+
62
+ before(:each) do
63
+ invoker_setup.distro_installer = distro_installer
64
+ mock_socat_scripts
65
+ end
32
66
 
33
67
  it "should create proper config file" do
34
68
  invoker_setup.expects(:initialize_distro_installer).returns(true)
@@ -46,10 +80,24 @@ describe Invoker::Power::LinuxSetup do
46
80
  expect(dnsmasq_content.strip).to_not be_empty
47
81
  expect(dnsmasq_content).to match(/dev/)
48
82
 
49
- rinetd_content = File.read(distro_installer.rinetd_file)
50
- expect(rinetd_content.strip).to_not be_empty
51
- expect(rinetd_content.strip).to match(/#{config.https_port}/)
52
- expect(rinetd_content.strip).to match(/#{config.http_port}/)
83
+ socat_content = File.read(Invoker::Power::Distro::Base::SOCAT_SHELLSCRIPT)
84
+ expect(socat_content.strip).to_not be_empty
85
+ expect(socat_content.strip).to match(/#{config.https_port}/)
86
+ expect(socat_content.strip).to match(/#{config.http_port}/)
87
+
88
+ service_file = File.read(Invoker::Power::Distro::Base::SOCAT_SYSTEMD)
89
+ expect(service_file.strip).to_not be_empty
90
+ end
91
+ end
92
+
93
+ describe 'resolver file' do
94
+ context 'user sets up a custom top level domain' do
95
+ it 'should create the correct resolver file' do
96
+ linux_setup = Invoker::Power::LinuxSetup.new('local')
97
+ suse_installer = Invoker::Power::Distro::Opensuse.new('local')
98
+ linux_setup.distro_installer = suse_installer
99
+ expect(linux_setup.resolver_file).to eq('/etc/dnsmasq.d/local-tld')
100
+ end
53
101
  end
54
102
  end
55
103
  end
@@ -1,9 +1,14 @@
1
1
  require "spec_helper"
2
2
 
3
- describe Invoker::Power::OsxSetup do
4
- describe "When no setup exists" do
3
+ describe Invoker::Power::OsxSetup, fakefs: true do
4
+ before do
5
+ FileUtils.mkdir_p(inv_conf_dir)
6
+ FileUtils.mkdir_p(Invoker::Power::OsxSetup::RESOLVER_DIR)
7
+ end
8
+
9
+ describe "when no setup exists" do
5
10
  it "should create a config file with port etc" do
6
- setup = Invoker::Power::OsxSetup.new
11
+ setup = Invoker::Power::OsxSetup.new('dev')
7
12
  setup.expects(:install_resolver).returns(true)
8
13
  setup.expects(:drop_to_normal_user).returns(true)
9
14
  setup.expects(:install_firewall).once
@@ -23,16 +28,16 @@ describe Invoker::Power::OsxSetup do
23
28
  fl.write("foo test")
24
29
  }
25
30
  Invoker::Power::Setup.any_instance.expects(:setup_invoker).never
26
- Invoker::Power::Setup.install()
31
+ Invoker::Power::Setup.install('dev')
27
32
  end
28
33
  end
29
34
 
30
35
  describe "when pow like setup exists" do
31
36
  before {
32
- File.open(Invoker::Power::OsxSetup::RESOLVER_FILE, "w") {|fl|
37
+ File.open(File.join(Invoker::Power::OsxSetup::RESOLVER_DIR, "dev"), "w") { |fl|
33
38
  fl.write("hello")
34
39
  }
35
- @setup = Invoker::Power::OsxSetup.new
40
+ @setup = Invoker::Power::OsxSetup.new('dev')
36
41
  }
37
42
 
38
43
  describe "when user selects to overwrite it" do
@@ -60,7 +65,7 @@ describe Invoker::Power::OsxSetup do
60
65
 
61
66
  describe "uninstalling firewall rules" do
62
67
  it "should uninstall firewall rules and remove all files created by setup" do
63
- setup = Invoker::Power::OsxSetup.new
68
+ setup = Invoker::Power::OsxSetup.new('dev')
64
69
 
65
70
  Invoker::CLI::Question.expects(:agree).returns(true)
66
71
  setup.expects(:remove_resolver_file).once
@@ -74,7 +79,7 @@ describe Invoker::Power::OsxSetup do
74
79
  describe "setup on fresh osx install" do
75
80
  context "when resolver directory does not exist" do
76
81
  before do
77
- @setup = Invoker::Power::OsxSetup.new
82
+ @setup = Invoker::Power::OsxSetup.new('dev')
78
83
  FileUtils.rm_rf(Invoker::Power::OsxSetup::RESOLVER_DIR)
79
84
  end
80
85
 
@@ -88,4 +93,13 @@ describe Invoker::Power::OsxSetup do
88
93
  end
89
94
  end
90
95
  end
96
+
97
+ describe '.resolver_file' do
98
+ context 'user sets up a custom top level domain' do
99
+ it 'should create the correct resolver file' do
100
+ setup = Invoker::Power::OsxSetup.new('local')
101
+ expect(setup.resolver_file).to eq('/etc/resolver/local')
102
+ end
103
+ end
104
+ end
91
105
  end
@@ -4,7 +4,18 @@ describe Invoker::Power::UrlRewriter do
4
4
  let(:rewriter) { Invoker::Power::UrlRewriter.new }
5
5
 
6
6
  context "matching domain part of incoming request" do
7
- it "should do foo.dev match" do
7
+ before(:all) do
8
+ @original_invoker_config = Invoker.config
9
+
10
+ Invoker.config = mock
11
+ Invoker.config.stubs(:tld).returns("dev")
12
+ end
13
+
14
+ after(:all) do
15
+ Invoker.config = @original_invoker_config
16
+ end
17
+
18
+ it "should match foo.dev" do
8
19
  match = rewriter.extract_host_from_domain("foo.dev")
9
20
  expect(match).to_not be_empty
10
21
 
@@ -34,5 +45,26 @@ describe Invoker::Power::UrlRewriter do
34
45
 
35
46
  expect(match[0]).to eq("hello-world")
36
47
  end
48
+
49
+ context 'user sets up a custom top level domain' do
50
+ before(:all) do
51
+ @original_invoker_config = Invoker.config
52
+
53
+ Invoker.config = mock
54
+ Invoker.config.stubs(:tld).returns("local")
55
+ end
56
+
57
+ it 'should match domain part of incoming request correctly' do
58
+ match = rewriter.extract_host_from_domain("foo.local")
59
+ expect(match).to_not be_empty
60
+
61
+ matching_string = match[0]
62
+ expect(matching_string).to eq("foo")
63
+ end
64
+
65
+ after(:all) do
66
+ Invoker.config = @original_invoker_config
67
+ end
68
+ end
37
69
  end
38
70
  end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ # Full integration test. Start a server, and client. Let client interact with
4
+ # server do a ping-pong. Client checks whether ping pong is successful or not.
5
+ # Also, mock rewriter so that it returns valid port for request proxying.
6
+ # - Server will run on port 28080.
7
+ # - Balancer will run on port 28081 proxying to 28080
8
+ # - Client will connect to 28081 performing ping-pong
9
+
10
+ def websocket_server
11
+ require 'websocket-eventmachine-server'
12
+
13
+ EM.run do
14
+ WebSocket::EventMachine::Server.start(host: "0.0.0.0", port: 28080) do |ws|
15
+ ws.onerror { |e| p e }
16
+ ws.onmessage { ws.send "pong" }
17
+ end
18
+
19
+ EM.add_timer(2) { EM.stop }
20
+ end
21
+ end
22
+
23
+ def websocket_client
24
+ require 'websocket-eventmachine-client'
25
+
26
+ @message = ""
27
+
28
+ EM.run do
29
+ ws = WebSocket::EventMachine::Client.connect(uri: 'ws://0.0.0.0:28081')
30
+ ws.onerror { |e| p e }
31
+ ws.onopen { ws.send("ping") }
32
+ ws.onmessage { |m, _| @message = m }
33
+
34
+ EM.add_timer(2) do
35
+ expect(@message).to eq "pong"
36
+ EM.stop
37
+ end
38
+ end
39
+ end
40
+
41
+
42
+ describe 'Web sockets support' do
43
+ it 'can ping pong via balancer' do
44
+ dns_response = Struct.new(:port, :ip).new(28080, "0.0.0.0")
45
+ Invoker::Power::UrlRewriter.any_instance
46
+ .stubs(:select_backend_config)
47
+ .returns(dns_response)
48
+
49
+ EM.run do
50
+ EM.start_server("0.0.0.0", 28081, EM::ProxyServer::Connection, {}) do |conn|
51
+ Invoker::Power::Balancer.new(conn, "http").install_callbacks
52
+ end
53
+
54
+ fork { websocket_server }
55
+ fork { websocket_client }
56
+ EM.add_timer(3) { EM.stop }
57
+ end
58
+
59
+ Process.waitall
60
+ end
61
+ end
@@ -2,13 +2,19 @@ require "spec_helper"
2
2
 
3
3
  describe Invoker::ProcessManager do
4
4
  let(:process_manager) { Invoker::ProcessManager.new }
5
+
5
6
  describe "#start_process_by_name" do
6
7
  it "should find command by label and start it, if found" do
7
- invoker_config.stubs(:processes).returns([OpenStruct.new(:label => "resque", :cmd => "foo", :dir => "bar")])
8
- invoker_config.expects(:process).returns(OpenStruct.new(:label => "resque", :cmd => "foo", :dir => "bar"))
8
+ @original_invoker_config = Invoker.config
9
+ Invoker.config = mock
10
+
11
+ Invoker.config.stubs(:processes).returns([OpenStruct.new(:label => "resque", :cmd => "foo", :dir => "bar")])
12
+ Invoker.config.expects(:process).returns(OpenStruct.new(:label => "resque", :cmd => "foo", :dir => "bar"))
9
13
  process_manager.expects(:start_process).returns(true)
10
14
 
11
15
  process_manager.start_process_by_name("resque")
16
+
17
+ Invoker.config = @original_invoker_config
12
18
  end
13
19
 
14
20
  it "should not start already running process" do
@@ -94,5 +100,31 @@ BAR=foo
94
100
  dir = "/tmp"
95
101
  expect(process_manager.load_env(dir)).to eq({})
96
102
  end
103
+
104
+ it "should load .local.env file if it exists" do
105
+ dir = "/tmp"
106
+ begin
107
+ env_file = File.new("#{dir}/.env", "w")
108
+ env_data =<<-EOD
109
+ FOO=foo
110
+ BAR=bar
111
+ EOD
112
+ env_file.write(env_data)
113
+ env_file.close
114
+
115
+ local_env_file = File.new("#{dir}/.env.local", "w")
116
+ local_env_data =<<-EOD
117
+ FOO=emacs
118
+ EOD
119
+ local_env_file.write(local_env_data)
120
+ local_env_file.close
121
+
122
+ env_options = process_manager.load_env(dir)
123
+ expect(env_options).to include("FOO" => "emacs", "BAR" => "bar")
124
+ ensure
125
+ File.delete(env_file.path)
126
+ File.delete(local_env_file.path)
127
+ end
128
+ end
97
129
  end
98
130
  end