appserver 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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