appserver 0.0.1 → 0.0.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.
- data/.gitignore +3 -0
- data/README.md +30 -9
- data/Rakefile +31 -0
- data/bin/appserver +9 -6
- data/lib/appserver.rb +10 -6
- data/lib/appserver/app.rb +106 -65
- data/lib/appserver/appserver.conf.rb +126 -0
- data/lib/appserver/command.rb +38 -10
- data/lib/appserver/configurator.rb +38 -0
- data/lib/appserver/logrotate.rb +46 -0
- data/lib/appserver/monit.rb +45 -0
- data/lib/appserver/nginx.rb +71 -0
- data/lib/appserver/repository.rb +80 -21
- data/lib/appserver/server_dir.rb +100 -0
- data/lib/appserver/unicorn.conf.rb +23 -27
- data/lib/appserver/utils.rb +24 -15
- data/test/apps/rack-simple/config.ru +7 -0
- data/test/apps/sinatra/Gemfile +4 -0
- data/test/apps/sinatra/config.ru +2 -0
- data/test/apps/sinatra/hello.rb +9 -0
- data/test/apps/sinatra/views/index.erb +1 -0
- data/test/helper.rb +49 -0
- data/test/unit/test_app.rb +72 -0
- data/test/unit/test_appserver.rb +26 -0
- data/test/unit/test_command.rb +64 -0
- data/test/unit/test_configurator.rb +116 -0
- data/test/unit/test_logrotate.rb +14 -0
- data/test/unit/test_monit.rb +13 -0
- data/test/unit/test_nginx.rb +13 -0
- data/test/unit/test_repository.rb +50 -0
- data/test/unit/test_server_dir.rb +121 -0
- data/test/unit/test_unicorn_conf.rb +58 -0
- data/test/unit/test_utils.rb +36 -0
- metadata +84 -11
- data/lib/appserver/appserver.yml +0 -85
- data/lib/appserver/server.rb +0 -136
@@ -1,18 +1,18 @@
|
|
1
1
|
require File.expand_path('../../appserver', __FILE__)
|
2
|
-
# We assume that the last argument to unicorn is the full path to config.ru
|
3
|
-
|
4
|
-
app = Appserver::
|
2
|
+
# We assume that the last argument to unicorn is the full path to the app's config.ru
|
3
|
+
app_path = File.dirname(Unicorn::HttpServer::START_CTX[:argv][-1])
|
4
|
+
app = Appserver::ServerDir.discover(app_path).app(File.basename(app_path))
|
5
5
|
|
6
|
-
working_directory app.
|
6
|
+
working_directory app.path
|
7
7
|
stderr_path app.server_log
|
8
8
|
stdout_path app.server_log
|
9
|
-
|
9
|
+
app.setup_env!
|
10
10
|
pid app.pid_file
|
11
11
|
listen "unix:#{app.socket}", :backlog => 64
|
12
|
-
|
12
|
+
user app.user, app.group
|
13
13
|
worker_processes app.instances
|
14
|
+
preload_app app.preload
|
14
15
|
timeout 30
|
15
|
-
preload_app true
|
16
16
|
|
17
17
|
# Use COW-friendly REE for memory saving, especially with preloaded apps
|
18
18
|
# http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
|
@@ -25,26 +25,22 @@ before_fork do |server, worker|
|
|
25
25
|
ActiveRecord::Base.connection.disconnect! if defined?(ActiveRecord::Base)
|
26
26
|
end
|
27
27
|
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
#
|
43
|
-
|
44
|
-
# end
|
45
|
-
#
|
46
|
-
# # *optionally* throttle the master from forking too quickly by sleeping
|
47
|
-
# sleep 1
|
28
|
+
# This allows a new master process to incrementally
|
29
|
+
# phase out the old master process with SIGTTOU to avoid a
|
30
|
+
# thundering herd (especially in the "preload_app false" case)
|
31
|
+
# when doing a transparent upgrade. The last worker spawned
|
32
|
+
# will then kill off the old master process with a SIGQUIT.
|
33
|
+
old_pid = "#{server.config[:pid]}.oldbin"
|
34
|
+
if old_pid != server.pid
|
35
|
+
begin
|
36
|
+
sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
|
37
|
+
Process.kill(sig, File.read(old_pid).to_i)
|
38
|
+
rescue Errno::ENOENT, Errno::ESRCH
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# *optionally* throttle the master from forking too quickly by sleeping
|
43
|
+
sleep 1
|
48
44
|
end
|
49
45
|
|
50
46
|
after_fork do |server, worker|
|
data/lib/appserver/utils.rb
CHANGED
@@ -1,15 +1,32 @@
|
|
1
|
+
require 'socket'
|
1
2
|
require 'tempfile'
|
2
3
|
|
3
4
|
module Appserver
|
4
5
|
module Utils
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def find_in_path (name)
|
9
|
+
ENV['PATH'].split(':').each do |path|
|
10
|
+
fn = File.join(path, name)
|
11
|
+
return fn if File.executable?(fn)
|
12
|
+
end
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def system_hostname
|
17
|
+
Socket.gethostname
|
18
|
+
end
|
19
|
+
|
20
|
+
def system_domainname
|
21
|
+
system_hostname.sub(/^[^.]+\./, '')
|
22
|
+
end
|
23
|
+
|
24
|
+
def number_of_cpus
|
25
|
+
if File.exist?('/proc/cpuinfo')
|
26
|
+
File.readlines('/proc/cpuinfo').grep(/^processor\s+:\s+\d+/).size
|
27
|
+
end
|
9
28
|
end
|
10
|
-
end
|
11
29
|
|
12
|
-
module Methods
|
13
30
|
def safe_replace_file (filename)
|
14
31
|
tempfile = Tempfile.new(File.basename(filename) + '.', File.dirname(filename))
|
15
32
|
if File.exist?(filename)
|
@@ -19,15 +36,7 @@ module Appserver
|
|
19
36
|
yield tempfile
|
20
37
|
tempfile.close
|
21
38
|
File.unlink(filename) if File.exist?(filename)
|
22
|
-
File.rename(tempfile, filename)
|
23
|
-
end
|
24
|
-
|
25
|
-
def symbolize_keys (hash)
|
26
|
-
hash.inject({}) do |memo, (key, value)|
|
27
|
-
value = symbolize_keys(value) if Hash === value
|
28
|
-
memo[key.to_sym] = value
|
29
|
-
memo
|
30
|
-
end
|
39
|
+
File.rename(tempfile.path, filename)
|
31
40
|
end
|
32
41
|
end
|
33
42
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Hello world from Sinatra
|
data/test/helper.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'test-unit'
|
3
|
+
require 'test/unit'
|
4
|
+
require 'mocha'
|
5
|
+
|
6
|
+
require 'appserver'
|
7
|
+
require 'tmpdir'
|
8
|
+
require 'git'
|
9
|
+
|
10
|
+
class Test::Unit::TestCase
|
11
|
+
|
12
|
+
SAMPLE_APPS_PATH = File.expand_path('../apps', __FILE__)
|
13
|
+
|
14
|
+
# Runs the given block in an empty, temporary direcory
|
15
|
+
def in_empty_dir (&block)
|
16
|
+
Dir.mktmpdir do |tmpdir|
|
17
|
+
Dir.chdir(tmpdir) do
|
18
|
+
yield
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Runs the given block in an initialized appserver directory
|
24
|
+
def in_server_dir (&block)
|
25
|
+
in_empty_dir do
|
26
|
+
yield Appserver::ServerDir.init('.', :force => true)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Copies the named sample app to the given path
|
31
|
+
def create_app (name, path)
|
32
|
+
raise 'Target path already exist' if File.exist?(path)
|
33
|
+
FileUtils.cp_r File.join(SAMPLE_APPS_PATH, name), path
|
34
|
+
end
|
35
|
+
|
36
|
+
# Creates a bare git repository of the named sample app
|
37
|
+
def create_app_repo (name, path)
|
38
|
+
raise 'Target path already exist' if File.exist?(path)
|
39
|
+
path = File.expand_path(path)
|
40
|
+
Dir.chdir(File.join(SAMPLE_APPS_PATH, name)) do
|
41
|
+
raise 'Already a git repository' if File.exist?('.git')
|
42
|
+
git = Git.init('.')
|
43
|
+
git.add('.')
|
44
|
+
git.commit('Initial commit')
|
45
|
+
git.config('core.bare', true)
|
46
|
+
FileUtils.mv '.git', path
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'bundler'
|
3
|
+
|
4
|
+
class TestApp < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@env = { 'PATH' => '/some/where:/no/where', 'FOO_KEY' => 'FooFoo', 'MY_DIR' => '/no/where' }
|
8
|
+
Appserver::App.send(:const_set, :ENV, @env)
|
9
|
+
end
|
10
|
+
|
11
|
+
def teardown
|
12
|
+
Appserver::App.send(:remove_const, :ENV)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_default_hostname
|
16
|
+
in_server_dir do |server_dir|
|
17
|
+
Appserver::App::SETTINGS_DEFAULTS[:domain] = 'example.com'
|
18
|
+
app = server_dir.app('myapp')
|
19
|
+
assert_equal 'example.com', app.domain
|
20
|
+
assert_equal 'myapp.example.com', app.hostname
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_setup_env_default_environment
|
25
|
+
in_server_dir do |server_dir|
|
26
|
+
app = server_dir.app('myapp')
|
27
|
+
assert_equal([], app.env_whitelist)
|
28
|
+
assert_equal({}, app.env)
|
29
|
+
app.setup_env!
|
30
|
+
assert_equal({ 'PATH' => '/some/where:/no/where' }, @env)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_setup_env_whitelist
|
35
|
+
in_server_dir do |server_dir|
|
36
|
+
app = server_dir.app('myapp')
|
37
|
+
app.stubs(:env_whitelist => ['MY_DIR'])
|
38
|
+
app.setup_env!
|
39
|
+
assert_equal({ 'PATH' => '/some/where:/no/where', 'MY_DIR' => '/no/where' }, @env)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_setup_env_full_environment
|
44
|
+
in_server_dir do |server_dir|
|
45
|
+
app = server_dir.app('myapp')
|
46
|
+
app.stubs(:env_whitelist => '*')
|
47
|
+
app.setup_env!
|
48
|
+
assert_equal({ 'PATH' => '/some/where:/no/where', 'FOO_KEY' => 'FooFoo', 'MY_DIR' => '/no/where' }, @env)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_setup_env_additions
|
53
|
+
in_server_dir do |server_dir|
|
54
|
+
app = server_dir.app('myapp')
|
55
|
+
app.stubs(:env => { 'SOME_KEY' => 'secret' })
|
56
|
+
app.setup_env!
|
57
|
+
assert_equal({ 'PATH' => '/some/where:/no/where', 'SOME_KEY' => 'secret' }, @env)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_setup_env_sets_up_gem_bundle
|
62
|
+
in_server_dir do |server_dir|
|
63
|
+
app = server_dir.app('myapp')
|
64
|
+
File.stubs(:exist?).with(app.gem_file).returns(true)
|
65
|
+
File.stubs(:directory?).with(app.bundle_path).returns(true)
|
66
|
+
Bundler.expects(:setup)
|
67
|
+
app.setup_env!
|
68
|
+
assert_not_nil @env['BUNDLE_PATH']
|
69
|
+
assert_not_nil @env['GEM_HOME']
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestAppserver < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def appserver (*args)
|
6
|
+
cmd = File.expand_path('../../../bin/appserver', __FILE__)
|
7
|
+
bind = TOPLEVEL_BINDING.dup
|
8
|
+
bind.eval("Object.send(:remove_const, :ARGV); ARGV = #{args.inspect}", __FILE__, __LINE__)
|
9
|
+
bind.eval(File.read(cmd), cmd)
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_command
|
13
|
+
Appserver::Command.expects(:run!).with('doit', ['anarg', 'anotherarg'], {})
|
14
|
+
appserver('doit', 'anarg', 'anotherarg')
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_dir_option
|
18
|
+
Appserver::Command.expects(:run!).with('doit', [], { :dir => '/path/to/anywhere' })
|
19
|
+
appserver('--dir', '/path/to/anywhere', 'doit')
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_force_option
|
23
|
+
Appserver::Command.expects(:run!).with('doit', [], { :force => true })
|
24
|
+
appserver('--force', 'doit')
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestCommand < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@app = stub('App', :branch => 'thebranch')
|
7
|
+
@repository = stub('Repository', :app => @app)
|
8
|
+
@server_dir = stub('ServerDir', :repository => @repository)
|
9
|
+
Appserver::ServerDir.stubs(:discover => @server_dir)
|
10
|
+
# FIXME: This is currently needed to silence appserver output during tests :(
|
11
|
+
Appserver::Command.any_instance.stubs(:puts)
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_unknown_command
|
15
|
+
assert_raise Appserver::UnknownCommandError do
|
16
|
+
Appserver::Command.run!('foo', [])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_dir_option
|
21
|
+
Dir.expects(:chdir).with('thedir')
|
22
|
+
@server_dir.stubs(:write_configs)
|
23
|
+
Appserver::Command.run!('update', [], :dir => 'thedir')
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_init
|
27
|
+
Appserver::ServerDir.expects(:init).with('thedir', {})
|
28
|
+
Dir.expects(:chdir).with('thedir')
|
29
|
+
@server_dir.expects(:write_configs)
|
30
|
+
Appserver::Command.run!('init', ['thedir'])
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_command_discovers
|
34
|
+
Appserver::ServerDir.expects(:discover).returns(@server_dir)
|
35
|
+
@server_dir.expects(:write_configs)
|
36
|
+
Appserver::Command.run!('update', [])
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_update
|
40
|
+
@server_dir.expects(:write_configs)
|
41
|
+
Appserver::Command.run!('update', [])
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_deploy_manually
|
45
|
+
@server_dir.expects(:repository).with('repo.git').returns(@repository)
|
46
|
+
@repository.expects(:install_hook)
|
47
|
+
@repository.expects(:deploy).with('thebranch')
|
48
|
+
Appserver::Command.run!('deploy', ['repo.git'])
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_deploy_on_update
|
52
|
+
@server_dir.expects(:repository).with('repo.git').returns(@repository)
|
53
|
+
@repository.expects(:install_hook)
|
54
|
+
@repository.expects(:deploy).with('0123456789abcdef')
|
55
|
+
Appserver::Command.run!('deploy', ['repo.git', '/refs/heads/thebranch', '0123456789abcdef'])
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_deploy_on_update_of_different_branch_does_nothing
|
59
|
+
@server_dir.expects(:repository).with('repo.git').returns(@repository)
|
60
|
+
@repository.expects(:install_hook)
|
61
|
+
@repository.expects(:deploy).never
|
62
|
+
Appserver::Command.run!('deploy', ['repo.git', '/refs/heads/xyz', '0123456789abcdef'])
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestConfigurator < Test::Unit::TestCase
|
4
|
+
|
5
|
+
DEFAULTS = {
|
6
|
+
:alpha => 'aaa',
|
7
|
+
:beta => 'bbb',
|
8
|
+
:gamma => 'ccc',
|
9
|
+
:delta => 'ddd',
|
10
|
+
}
|
11
|
+
|
12
|
+
CONFIG_TEXT = <<-EOF
|
13
|
+
alpha 'anna'
|
14
|
+
beta 'bob'
|
15
|
+
context 'weird' do
|
16
|
+
beta 'betty'
|
17
|
+
gamma 'georg'
|
18
|
+
end
|
19
|
+
EOF
|
20
|
+
|
21
|
+
class ConfigTarget < Struct.new(:alpha, :beta, :gamma, :delta)
|
22
|
+
SETTINGS_DEFAULTS = DEFAULTS
|
23
|
+
SETTINGS_EXPAND = []
|
24
|
+
end
|
25
|
+
|
26
|
+
def configurator (config_text, *args)
|
27
|
+
config_file = '/tmp/some/path/test.conf.rb'
|
28
|
+
File.stubs(:read).with(config_file).returns(config_text)
|
29
|
+
Appserver::Configurator.new(config_file, *args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def setup
|
33
|
+
@config = configurator(CONFIG_TEXT)
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_default_setting
|
37
|
+
assert_no_match /delta/, CONFIG_TEXT
|
38
|
+
@config.apply!(target = ConfigTarget.new)
|
39
|
+
assert_equal DEFAULTS[:delta], target.delta
|
40
|
+
@config.apply!(target = ConfigTarget.new, 'weird')
|
41
|
+
assert_equal DEFAULTS[:delta], target.delta
|
42
|
+
@config.apply!(target = ConfigTarget.new, 'away')
|
43
|
+
assert_equal DEFAULTS[:delta], target.delta
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_global_setting_is_global
|
47
|
+
@config.apply!(target = ConfigTarget.new)
|
48
|
+
assert_equal 'anna', target.alpha
|
49
|
+
@config.apply!(target = ConfigTarget.new, 'weird')
|
50
|
+
assert_equal 'anna', target.alpha
|
51
|
+
@config.apply!(target = ConfigTarget.new, 'away')
|
52
|
+
assert_equal 'anna', target.alpha
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_context_setting_overrides_global_setting
|
56
|
+
@config.apply!(target = ConfigTarget.new)
|
57
|
+
assert_equal 'bob', target.beta
|
58
|
+
@config.apply!(target = ConfigTarget.new, 'weird')
|
59
|
+
assert_equal 'betty', target.beta
|
60
|
+
@config.apply!(target = ConfigTarget.new, 'away')
|
61
|
+
assert_equal 'bob', target.beta
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_context_setting_overrides_default
|
65
|
+
@config.apply!(target = ConfigTarget.new)
|
66
|
+
assert_equal DEFAULTS[:gamma], target.gamma
|
67
|
+
@config.apply!(target = ConfigTarget.new, 'weird')
|
68
|
+
assert_equal 'georg', target.gamma
|
69
|
+
@config.apply!(target = ConfigTarget.new, 'away')
|
70
|
+
assert_equal DEFAULTS[:gamma], target.gamma
|
71
|
+
end
|
72
|
+
|
73
|
+
class ConfigTargetWithExpand < ConfigTarget
|
74
|
+
SETTINGS_EXPAND = [ :beta, :gamma ]
|
75
|
+
def path; '/tmp/some/path'; end
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_settings_expand
|
79
|
+
@config.apply!(target = ConfigTargetWithExpand.new)
|
80
|
+
assert_equal 'anna', target.alpha
|
81
|
+
assert_equal '/tmp/some/path/bob', target.beta
|
82
|
+
assert_equal '/tmp/some/path/ccc', target.gamma
|
83
|
+
assert_equal 'ddd', target.delta
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_any_setting_is_possible_by_default
|
87
|
+
config = configurator("alpha 'anna'; beta 'bob'")
|
88
|
+
config.apply!(target = ConfigTarget.new)
|
89
|
+
assert_equal 'anna', target.alpha
|
90
|
+
assert_equal 'bob', target.beta
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_allowed_global_settings
|
94
|
+
assert_nothing_raised do
|
95
|
+
configurator("alpha 'anna'; beta 'bob'", [ :alpha, :beta ])
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def test_allowed_global_settings_fail_on_invalid_setting
|
100
|
+
assert_raise NoMethodError do
|
101
|
+
configurator("alpha 'anna'; beta 'bob'", [ :alpha ])
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_allowed_context_settings
|
106
|
+
assert_nothing_raised do
|
107
|
+
configurator("context 'foo' do; alpha 'anna'; beta 'bob'; end", nil, [ :alpha, :beta ])
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_allowed_context_settings_fail_on_invalid_setting
|
112
|
+
assert_raise NoMethodError do
|
113
|
+
configurator("context 'foo' do; alpha 'anna'; beta 'bob'; end", nil, [ :alpha ])
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|