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.
@@ -15,25 +15,53 @@ module Appserver
15
15
  def run!
16
16
  Dir.chdir(options[:dir]) if options[:dir]
17
17
 
18
- Server.initialize_dir(options) if command == 'init'
18
+ if command == 'init'
19
+ ServerDir.init(arguments[0], options)
20
+ Dir.chdir(arguments[0])
21
+ end
19
22
 
20
- server = Server.new(options)
23
+ server_dir = ServerDir.discover
21
24
 
22
25
  case command
23
26
  when 'init'
24
- server.write_configs
27
+ server_dir.write_configs
25
28
  puts 'Initialized appserver directory.'
26
- puts 'Wrote Monit and Nginx configuration snippets. Make sure to include them into'
27
- puts 'your system\'s Monit and Nginx configuration to become active.'
29
+ puts 'Wrote configuration snippets. Make sure to include them into your'
30
+ puts 'system\'s Monit/Nginx/Logrotate configuration to become active.'
31
+
32
+ when 'update'
33
+ server_dir.write_configs
34
+ puts 'Wrote configuration snippets.'
28
35
 
29
36
  when 'deploy'
30
- repository = server.repository(arguments[0])
31
- # TODO
37
+ repository = server_dir.repository(arguments[0])
32
38
  repository.install_hook
39
+ # Second and third arguments are used by git update hooks and contain
40
+ # the ref name and the new ref that just have been updated
41
+ ref = repository.app.branch
42
+ if arguments[1] && arguments[2]
43
+ return unless arguments[1] =~ %r(refs/heads/#{ref})
44
+ ref = arguments[2]
45
+ end
46
+ puts 'Deploying application...'
47
+ repository.deploy(ref)
48
+ puts 'Done.'
33
49
 
34
- when 'update'
35
- server.write_configs
36
- puts 'Wrote Monit and Nginx configuration snippets.'
50
+ when 'start'
51
+ app = server_dir.app(arguments[0])
52
+ app.start_server
53
+
54
+ when 'stop'
55
+ app = server_dir.app(arguments[0])
56
+ app.stop_server
57
+
58
+ when 'restart'
59
+ app = server_dir.app(arguments[0])
60
+ app.restart_server
61
+
62
+ when 'reopen'
63
+ app = server_dir.app(arguments[0])
64
+ app.reopen_server_log
37
65
 
38
66
  else
39
67
  raise UnknownCommandError
@@ -0,0 +1,38 @@
1
+ module Appserver
2
+ class Configurator < Struct.new(:settings, :global_keys, :context_keys)
3
+
4
+ def initialize (config_file, global_keys = nil, context_keys = nil)
5
+ self.settings = {}
6
+ self.global_keys = global_keys
7
+ self.context_keys = context_keys
8
+ instance_eval(File.read(config_file), config_file) if config_file
9
+ end
10
+
11
+ def apply! (target, context = nil)
12
+ settings = (self.settings[nil] || {}).dup
13
+ settings.update(self.settings[context] || {}) if context
14
+ target.class.const_get(:SETTINGS_DEFAULTS).each do |key, default_value|
15
+ value = settings[key] || default_value
16
+ value = File.expand_path(value, target.path) if value && target.class.const_get(:SETTINGS_EXPAND).include?(key)
17
+ target.send("#{key}=", value)
18
+ end
19
+ end
20
+
21
+ def context (context)
22
+ saved_context = @context
23
+ @context = context.to_s
24
+ yield
25
+ @context = saved_context
26
+ end
27
+ alias_method :app, :context
28
+
29
+ protected
30
+
31
+ def method_missing (method, *args)
32
+ return super if !@context && global_keys && !global_keys.include?(method)
33
+ return super if @context && context_keys && !context_keys.include?(method)
34
+ self.settings[@context] ||= {}
35
+ self.settings[@context][method] = args[0] if args.size > 0
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,46 @@
1
+ module Appserver
2
+ class Logrotate < Struct.new(:server_dir)
3
+
4
+ def self.write_config (server_dir)
5
+ new(server_dir).write_config
6
+ end
7
+
8
+ def initialize (server_dir)
9
+ self.server_dir = server_dir
10
+ end
11
+
12
+ def write_config
13
+ Utils.safe_replace_file(server_dir.logrotate_conf) do |f|
14
+ f.puts "# Logrotate configuration automagically generated by the \"appserver\" gem using"
15
+ f.puts "# the appserver directory config #{server_dir.config_file}"
16
+ f.puts "# Include this file into your system's logrotate.conf (using an include statement)"
17
+ f.puts "# to use it. See http://github.com/zargony/appserver for details."
18
+ # Handle access logs of Nginx in one statement, so Nginx only needs to reopen once
19
+ access_logs = server_dir.apps.map { |app| app.access_log }.compact
20
+ f.puts "#{access_logs.join(' ')} {"
21
+ f.puts " missingok"
22
+ f.puts " delaycompress"
23
+ f.puts " sharedscripts"
24
+ f.puts " postrotate"
25
+ f.puts " #{server_dir.nginx_reopen}"
26
+ f.puts " endscript"
27
+ f.puts "}"
28
+ # Add application-specific Logrotate configuration
29
+ server_dir.apps.each do |app|
30
+ f.puts ""
31
+ f.puts "# Application: #{app.name}"
32
+ if app.server_log
33
+ f.puts "#{app.server_log} {"
34
+ f.puts " missingok"
35
+ f.puts " delaycompress"
36
+ f.puts " sharedscripts"
37
+ f.puts " postrotate"
38
+ f.puts " #{server_dir.appserver_cmd('reload', app.name)}"
39
+ f.puts " endscript"
40
+ f.puts "}"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,45 @@
1
+ module Appserver
2
+ class Monit < Struct.new(:server_dir)
3
+
4
+ def self.write_config (server_dir)
5
+ new(server_dir).write_config
6
+ end
7
+
8
+ def initialize (server_dir)
9
+ self.server_dir = server_dir
10
+ end
11
+
12
+ def write_config
13
+ Utils.safe_replace_file(server_dir.monit_conf) do |f|
14
+ f.puts "# Monit configuration automagically generated by the \"appserver\" gem using"
15
+ f.puts "# the appserver directory config #{server_dir.config_file}"
16
+ f.puts "# Include this file into your system's monitrc (using an include statement)"
17
+ f.puts "# to use it. See http://github.com/zargony/appserver for details."
18
+ # Let Monit reload itself if this configuration changes
19
+ f.puts "check file monit_conf with path #{server_dir.monit_conf}"
20
+ f.puts " if changed checksum then exec \"#{server_dir.monit_reload}\""
21
+ # Reload Nginx if its configuration changes
22
+ f.puts "check file nginx_conf with path #{server_dir.nginx_conf}"
23
+ f.puts " if changed checksum then exec \"#{server_dir.nginx_reload}\""
24
+ # Add application-specific Monit configuration
25
+ server_dir.apps.each do |app|
26
+ f.puts ""
27
+ f.puts "# Application: #{app.name}"
28
+ if app.pid_file && app.start_server?
29
+ cyclecheck = app.usage_check_cycles ? " for #{app.usage_check_cycles} cycles" : ''
30
+ f.puts "check process #{app.name} with pidfile #{app.pid_file}"
31
+ f.puts " start program = \"#{server_dir.appserver_cmd('start', app.name)}\""
32
+ f.puts " stop program = \"#{server_dir.appserver_cmd('stop', app.name)}\""
33
+ f.puts " if totalcpu usage > #{app.max_cpu_usage}#{cyclecheck} then restart" if app.max_cpu_usage
34
+ f.puts " if totalmemory usage > #{app.max_memory_usage}#{cyclecheck} then restart" if app.max_memory_usage
35
+ f.puts " if failed unixsocket #{app.socket} protocol http request \"/\" timeout #{app.http_check_timeout} seconds then restart" if app.http_check_timeout
36
+ f.puts " if 5 restarts within 5 cycles then timeout"
37
+ f.puts " group appserver"
38
+ f.puts "check file #{app.name}_revision with path #{app.revision_file}"
39
+ f.puts " if changed checksum then exec \"#{server_dir.appserver_cmd('restart', app.name)}\""
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,71 @@
1
+ module Appserver
2
+ class Nginx < Struct.new(:server_dir)
3
+
4
+ def self.write_config (server_dir)
5
+ new(server_dir).write_config
6
+ end
7
+
8
+ def initialize (server_dir)
9
+ self.server_dir = server_dir
10
+ end
11
+
12
+ def write_config
13
+ Utils.safe_replace_file(server_dir.nginx_conf) do |f|
14
+ f.puts "# Nginx configuration automagically generated by the \"appserver\" gem using"
15
+ f.puts "# the appserver directory config #{server_dir.config_file}"
16
+ f.puts "# Include this file into your system's nginx.conf (using an include statement"
17
+ f.puts "# inside a http statement) to use it. See http://github.com/zargony/appserver"
18
+ f.puts "# for details."
19
+ # The default server always responds with 403 Forbidden
20
+ f.puts "server {"
21
+ f.puts " listen 80 default;"
22
+ f.puts " server_name _;"
23
+ f.puts " deny all;"
24
+ f.puts "}"
25
+ # Add application-specific Nginx configuration
26
+ server_dir.apps.each do |app|
27
+ f.puts ""
28
+ f.puts "# Application: #{app.name}"
29
+ if app.socket
30
+ f.puts "upstream #{app.name} {"
31
+ f.puts " server unix:#{app.socket} fail_timeout=0;"
32
+ f.puts "}"
33
+ write_server_definition(f, app)
34
+ write_server_definition(f, app, true) if app.ssl?
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ protected
41
+
42
+ def write_server_definition (f, app, ssl = false)
43
+ f.puts "server {"
44
+ f.puts " listen #{ssl ? 443 : 80};"
45
+ f.puts " server_name #{app.hostname};"
46
+ if ssl
47
+ f.puts " ssl on;"
48
+ f.puts " ssl_certificate #{app.ssl_cert};"
49
+ f.puts " ssl_certificate_key #{app.ssl_key};"
50
+ f.puts " ssl_session_timeout 5m;"
51
+ f.puts " ssl_protocols SSLv2 SSLv3 TLSv1;"
52
+ f.puts " ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;"
53
+ f.puts " ssl_prefer_server_ciphers on;"
54
+ end
55
+ f.puts " root #{app.public_path};"
56
+ f.puts " access_log #{app.access_log};"
57
+ # TODO: maintenance mode rewriting
58
+ f.puts " try_files $uri/index.html $uri.html $uri @#{app.name};"
59
+ f.puts " location @#{app.name} {"
60
+ f.puts " proxy_set_header X-Real-IP $remote_addr;"
61
+ f.puts " proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;"
62
+ f.puts " proxy_set_header X-Forwarded-Proto https;" if ssl
63
+ f.puts " proxy_set_header Host $http_host;"
64
+ f.puts " proxy_redirect off;"
65
+ f.puts " proxy_pass http://#{app.name};"
66
+ f.puts " }"
67
+ f.puts " error_page 500 502 503 504 /500.html;" if File.exist?(File.join(app.public_path, '500.html'))
68
+ f.puts "}"
69
+ end
70
+ end
71
+ end
@@ -1,48 +1,107 @@
1
+ require 'fileutils'
2
+ require 'git'
3
+
1
4
  module Appserver
2
- class Repository < Struct.new(:server, :dir)
3
- class InvalidRepositoryError < RuntimeError; end
5
+ class InvalidRepositoryError < RuntimeError; end
6
+ class ExecError < RuntimeError; end
4
7
 
5
- include Utils
8
+ class Repository < Struct.new(:server_dir, :path)
6
9
 
7
- def initialize (server, dir, config)
8
- self.server, self.dir = server, dir.chomp('/')
10
+ def initialize (server_dir, path, config)
11
+ self.server_dir, self.path = server_dir, path.chomp('/')
9
12
  raise InvalidRepositoryError unless valid?
10
13
  end
11
14
 
12
15
  def name
13
- File.basename(dir, '.git')
16
+ File.basename(path, '.git')
17
+ end
18
+
19
+ def app
20
+ # The app for this repository (app of same name)
21
+ @app ||= server_dir.app(name)
14
22
  end
15
23
 
16
24
  def valid?
17
- File.directory?(File.join(dir, 'hooks')) && File.directory?(File.join(dir, 'refs'))
25
+ name && name != '' &&
26
+ File.directory?(File.join(path, 'hooks')) &&
27
+ File.directory?(File.join(path, 'refs'))
18
28
  end
19
29
 
20
- def post_receive_hook
21
- File.join(dir, 'hooks', 'post-receive')
30
+ def update_hook
31
+ File.join(path, 'hooks', 'update')
22
32
  end
23
33
 
24
34
  def install_hook
25
- deploy_cmd = "#{File.expand_path($0)} -d #{server.dir} deploy #{dir}"
26
- puts deploy_cmd
27
- if !File.exist?(post_receive_hook) || !File.executable?(post_receive_hook)
28
- puts "Installing git post-receive hook to repository #{dir}..."
29
- safe_replace_file(post_receive_hook) do |f|
35
+ deploy_cmd = server_dir.appserver_cmd('deploy')
36
+ if !File.exist?(update_hook) || !File.executable?(update_hook)
37
+ puts "Installing git update hook to repository #{path}..."
38
+ Utils.safe_replace_file(update_hook) do |f|
30
39
  f.puts '#!/bin/sh'
31
- f.puts deploy_cmd
32
- f.chown File.stat(dir).uid, File.stat(dir).gid
40
+ f.puts "#{deploy_cmd} #{path} $1 $3"
41
+ f.chown File.stat(path).uid, File.stat(path).gid
33
42
  f.chmod 0755
34
43
  end
35
- elsif !File.readlines(post_receive_hook).any? { |line| line =~ /^#{Regexp.escape(deploy_cmd)}/ }
36
- puts "Couldn't install post-receive hook. Foreign hook script already present in repository #{dir}!"
44
+ elsif !File.readlines(update_hook).any? { |line| line =~ /^#{Regexp.escape(deploy_cmd)}/ }
45
+ puts "Couldn't install update hook. Foreign hook script already present in repository #{path}!"
37
46
  else
38
- #puts "Hook already installed in repository #{dir}"
47
+ #puts "Hook already installed in repository #{path}"
48
+ end
49
+ end
50
+
51
+ def deploy (ref = nil)
52
+ # Choose a temporary build directory on the same filesystem so that it
53
+ # can be easily renamed/moved to be the real application directory later
54
+ build_path, old_path = "#{app.path}.new", "#{app.path}.old"
55
+ begin
56
+ # Check out the current code
57
+ ref ||= app.branch
58
+ checkout(build_path, ref)
59
+ # Install gem bundle if a Gemfile exists
60
+ install_bundle(build_path)
61
+
62
+ # TODO: more deploy setup (write database config, ...)
63
+
64
+ # Replace the current application directory with the newly built one
65
+ FileUtils.rm_rf old_path
66
+ FileUtils.mv app.path, old_path if File.exist?(app.path)
67
+ FileUtils.mv build_path, app.path
68
+ ensure
69
+ # If anything broke and the build directory still exists, remove it
70
+ FileUtils.rm_rf build_path
71
+ # If anything broke and the app directory doesn't exist anymore, put the old directory in place
72
+ FileUtils.mv old_path, app.path if !File.exist?(app.path) && File.exist?(old_path)
39
73
  end
40
74
  end
41
75
 
42
76
  protected
43
77
 
44
- def expand_path (path)
45
- File.expand_path(path, dir)
78
+ def checkout (target_path, ref = 'master')
79
+ # There seem to be two ways to "export" the tip of a branch from a repository
80
+ # 1. clone the repository, check out the branch and remove the .git directory afterwards
81
+ #system("git clone --depth 1 --branch master #{path} #{target_path} && rm -rf #{target_path}/.git")
82
+ # 2. do a hard reset while pointing GIT_DIR to the repository and GIT_WORK_TREE to an empty dir
83
+ #system("mkdir #{target_path} && git --git-dir=#{path} --work-tree=#{target_path} reset --hard #{branch}")
84
+ git = Git.clone(path, target_path, :depth => 1)
85
+ ref = git.revparse(ref)
86
+ git.checkout(ref)
87
+ Dir.chdir(target_path) do
88
+ FileUtils.rm_rf '.git'
89
+ Utils.safe_replace_file 'REVISION' do |f|
90
+ f.puts ref
91
+ end
92
+ end
93
+ end
94
+
95
+ def install_bundle (target_path)
96
+ Dir.chdir(target_path) do
97
+ # Remove any .bundle subdirectory (it shouldn't be in the repository anyway)
98
+ FileUtils.rm_rf '.bundle'
99
+ # If there's a Gemfile, run "bundle install"
100
+ if File.exist?('Gemfile')
101
+ system "#{app.ruby} -S -- bundle install .bundle --without development test"
102
+ raise ExecError if $?.exitstatus > 0
103
+ end
104
+ end
46
105
  end
47
106
  end
48
107
  end
@@ -0,0 +1,100 @@
1
+ require 'fileutils'
2
+
3
+ module Appserver
4
+ class DirectoryAlreadyExistError < RuntimeError; end
5
+ class NotInitializedError < RuntimeError; end
6
+
7
+ class ServerDir < Struct.new(:path, :monit_conf, :monit_reload, :nginx_conf, :nginx_reload, :nginx_reopen, :logrotate_conf)
8
+
9
+ CONFIG_FILE_NAME = 'appserver.conf.rb'
10
+
11
+ SETTINGS_DEFAULTS = {
12
+ :monit_conf => 'monitrc',
13
+ :monit_reload => '/usr/sbin/monit reload',
14
+ :nginx_conf => 'nginx.conf',
15
+ :nginx_reload => '/usr/sbin/nginx -s reload',
16
+ :nginx_reopen => '/usr/sbin/nginx -s reopen',
17
+ :logrotate_conf => 'logrotate.conf',
18
+ }
19
+
20
+ SETTINGS_EXPAND = [ :monit_conf, :nginx_conf, :logrotate_conf ]
21
+
22
+ def self.config_file_template
23
+ File.expand_path("../#{CONFIG_FILE_NAME}", __FILE__)
24
+ end
25
+
26
+ def self.discover (path = '.', options = {})
27
+ if File.exist?(File.join(path, CONFIG_FILE_NAME))
28
+ new(path, options)
29
+ elsif path != '/'
30
+ discover(File.expand_path('..', path), options)
31
+ else
32
+ raise NotInitializedError
33
+ end
34
+ end
35
+
36
+ def self.init (path, options = {})
37
+ raise DirectoryAlreadyExistError if File.exist?(path) && !options[:force]
38
+ FileUtils.mkdir_p path
39
+ Dir.chdir(path) do
40
+ FileUtils.cp config_file_template, CONFIG_FILE_NAME
41
+ FileUtils.mkdir_p ['apps', 'tmp', 'log']
42
+ end
43
+ new(path, options)
44
+ end
45
+
46
+ def initialize (path, options = {})
47
+ self.path = File.expand_path(path)
48
+ # Load and apply configuration settings
49
+ app_keys = App::SETTINGS_DEFAULTS.keys
50
+ global_keys = SETTINGS_DEFAULTS.keys + App::SETTINGS_DEFAULTS.keys
51
+ @config = Configurator.new(File.exist?(config_file) ? config_file : nil, global_keys, app_keys)
52
+ @config.apply!(self)
53
+ end
54
+
55
+ def config_file
56
+ File.join(path, CONFIG_FILE_NAME)
57
+ end
58
+
59
+ def appserver_cmd (*args)
60
+ cmd = File.expand_path('../../../bin/appserver', __FILE__)
61
+ "#{cmd} -d #{path} #{args.join(' ')}"
62
+ end
63
+
64
+ def apps_path
65
+ File.join(path, 'apps')
66
+ end
67
+
68
+ def tmp_path
69
+ File.join(path, 'tmp')
70
+ end
71
+
72
+ def log_path
73
+ File.join(path, 'log')
74
+ end
75
+
76
+ def app (name)
77
+ @apps ||= {}
78
+ @apps[name] ||= App.new(self, name, @config)
79
+ end
80
+
81
+ def apps
82
+ Dir.glob(File.join(apps_path, '*')).
83
+ select { |f| File.directory?(f) }.
84
+ map { |f| File.basename(f) }.
85
+ reject { |f| f =~ /\.(tmp|old|new)$/ }.
86
+ map { |name| app(name) }
87
+ end
88
+
89
+ def repository (path)
90
+ @repositories ||= {}
91
+ @repositories[File.expand_path(path, self.path)] ||= Repository.new(self, path, @config)
92
+ end
93
+
94
+ def write_configs
95
+ Monit.write_config(self)
96
+ Nginx.write_config(self)
97
+ Logrotate.write_config(self)
98
+ end
99
+ end
100
+ end