ringleader 0.0.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/README.md +61 -22
  2. data/assets/app.js +106 -0
  3. data/assets/backbone-min.js +38 -0
  4. data/assets/bootstrap/css/bootstrap-responsive.css +815 -0
  5. data/assets/bootstrap/css/bootstrap-responsive.min.css +9 -0
  6. data/assets/bootstrap/css/bootstrap.css +4983 -0
  7. data/assets/bootstrap/css/bootstrap.min.css +9 -0
  8. data/assets/bootstrap/img/glyphicons-halflings-white.png +0 -0
  9. data/assets/bootstrap/img/glyphicons-halflings.png +0 -0
  10. data/assets/bootstrap/js/bootstrap.js +1825 -0
  11. data/assets/bootstrap/js/bootstrap.min.js +6 -0
  12. data/assets/favicon.ico +0 -0
  13. data/assets/index.html +129 -0
  14. data/assets/jquery-1.7.2.min.js +4 -0
  15. data/assets/jquery.mustache.js +636 -0
  16. data/assets/top_hat.png +0 -0
  17. data/assets/underscore-min.js +32 -0
  18. data/dev_scripts/Procfile +3 -2
  19. data/dev_scripts/stubborn.rb +4 -0
  20. data/dev_scripts/test.yml +4 -2
  21. data/dev_scripts/wait_fork_tree.rb +15 -0
  22. data/dev_scripts/wait_test.rb +25 -0
  23. data/dev_scripts/webserver.rb +4 -0
  24. data/lib/ringleader.rb +7 -1
  25. data/lib/ringleader/app.rb +88 -121
  26. data/lib/ringleader/app_serializer.rb +15 -0
  27. data/lib/ringleader/cli.rb +43 -56
  28. data/lib/ringleader/config.rb +58 -15
  29. data/lib/ringleader/controller.rb +30 -0
  30. data/lib/ringleader/process.rb +145 -0
  31. data/lib/ringleader/server.rb +116 -0
  32. data/lib/ringleader/version.rb +1 -1
  33. data/lib/ringleader/wait_for_exit.rb +1 -1
  34. data/ringleader.gemspec +1 -0
  35. data/screenshot.png +0 -0
  36. data/spec/fixtures/config.yml +1 -1
  37. data/spec/fixtures/no_app_port.yml +5 -0
  38. data/spec/fixtures/no_server_port.yml +6 -0
  39. data/spec/fixtures/rvm.yml +6 -0
  40. data/spec/ringleader/config_spec.rb +25 -2
  41. metadata +48 -3
  42. data/lib/ringleader/app_proxy.rb +0 -61
@@ -3,27 +3,51 @@ module Ringleader
3
3
 
4
4
  DEFAULT_IDLE_TIMEOUT = 1800
5
5
  DEFAULT_STARTUP_TIMEOUT = 30
6
- DEFAULT_HOSTNAME = "127.0.0.1"
7
- REQUIRED_KEYS = %w(dir command server_port app_port idle_timeout)
6
+ DEFAULT_HOST= "127.0.0.1"
7
+ REQUIRED_KEYS = %w(dir command app_port server_port)
8
8
 
9
- def initialize(file)
10
- @config = YAML.load(File.read(file))
9
+ TERMINAL_COLORS = [:red, :green, :yellow, :blue, :magenta, :cyan]
10
+
11
+ attr_reader :apps
12
+
13
+ # Public: Load the configs from a file
14
+ #
15
+ # file - the yml file to load the config from
16
+ # boring - use terminal colors instead of a rainbow for app colors
17
+ def initialize(file, boring=false)
18
+ config_data = YAML.load(File.read(file))
19
+ configs = convert_and_validate config_data, boring
20
+ @apps = Hash[*configs.flatten]
11
21
  end
12
22
 
13
- def apps
14
- unless @app
15
- configs = @config.map do |name, options|
16
- options["name"] = name
17
- options["hostname"] ||= DEFAULT_HOSTNAME
18
- options["idle_timeout"] ||= DEFAULT_IDLE_TIMEOUT
19
- options["startup_timeout"] ||= DEFAULT_STARTUP_TIMEOUT
20
- validate name, options
21
- [name, OpenStruct.new(options)]
23
+ # Private: convert a YML hash to an array of name/OpenStruct pairs
24
+ #
25
+ # Does validation for each app config and raises an error if anything is
26
+ # wrong. Sets default values for missing options, and assigns colors to each
27
+ # app config.
28
+ #
29
+ # configs - a hash of config data
30
+ # boring - whether or not to use a rainbow of colors for the apps
31
+ #
32
+ # Returns [ [app_name, OpenStruct], ... ]
33
+ def convert_and_validate(configs, boring)
34
+ assign_colors configs, boring
35
+ configs.map do |name, options|
36
+ options["name"] = name
37
+ options["host"] ||= DEFAULT_HOST
38
+ options["idle_timeout"] ||= DEFAULT_IDLE_TIMEOUT
39
+ options["startup_timeout"] ||= DEFAULT_STARTUP_TIMEOUT
40
+
41
+ if command = options.delete("rvm")
42
+ options["command"] = "source ~/.rvm/scripts/rvm && rvm --with-rubies rvmrc exec -- #{command}"
22
43
  end
23
44
 
24
- @apps = Hash[*configs.flatten]
45
+ validate name, options
46
+
47
+ options["dir"] = File.expand_path options["dir"]
48
+
49
+ [name, OpenStruct.new(options)]
25
50
  end
26
- @apps
27
51
  end
28
52
 
29
53
  # Private: validate that the options have all of the required keys
@@ -34,5 +58,24 @@ module Ringleader
34
58
  end
35
59
  end
36
60
  end
61
+
62
+ # Private: assign a color to each application configuration.
63
+ #
64
+ # configs - the config data to modify
65
+ # boring - use boring standard terminal colors instead of a rainbow.
66
+ def assign_colors(configs, boring)
67
+ sorted = configs.sort_by(&:first).map(&:last)
68
+ if boring
69
+ sorted.each.with_index do |config, i|
70
+ config["color"] = TERMINAL_COLORS[ i % TERMINAL_COLORS.length ]
71
+ end
72
+ else
73
+ offset = 360/configs.size
74
+ sorted.each.with_index do |config, i|
75
+ config["color"] = Color::HSL.new(offset * i, 100, 50).html
76
+ end
77
+ end
78
+ end
79
+
37
80
  end
38
81
  end
@@ -0,0 +1,30 @@
1
+ module Ringleader
2
+ class Controller
3
+ include Celluloid
4
+ include Celluloid::Logger
5
+
6
+ def initialize(configs)
7
+ @apps = {}
8
+ configs.each do |name, config|
9
+ @apps[name] = App.new(config)
10
+ end
11
+ end
12
+
13
+ def apps
14
+ @apps.values.sort_by { |a| a.name }
15
+ end
16
+
17
+ def app(name)
18
+ @apps[name]
19
+ end
20
+
21
+ def stop
22
+ exit if @stopping # if ctrl-c called twice...
23
+ @stopping = true
24
+ info "shutting down..."
25
+ @apps.values.map do |app|
26
+ Thread.new { app.stop }
27
+ end.map(&:join)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,145 @@
1
+ module Ringleader
2
+
3
+ # Represents an instance of a configured application.
4
+ class Process
5
+ include Celluloid
6
+ include Celluloid::Logger
7
+ include NameLogger
8
+
9
+ attr_reader :config
10
+
11
+ # Create a new App instance.
12
+ #
13
+ # config - a configuration object for this app
14
+ def initialize(config)
15
+ @config = config
16
+ @starting = @running = false
17
+ end
18
+
19
+ # Public: query if the app is running
20
+ def running?
21
+ @running
22
+ end
23
+
24
+ # Public: start the application.
25
+ #
26
+ # This method is intended to be used synchronously. If the app is already
27
+ # running, it'll return immediately. If the app hasn't been started, or is
28
+ # in the process of starting, this method blocks until it starts or fails to
29
+ # start correctly.
30
+ #
31
+ # Returns true if the app started, false if not.
32
+ def start
33
+ if @running
34
+ true
35
+ elsif @starting
36
+ wait :running
37
+ else
38
+ start_app
39
+ end
40
+ end
41
+
42
+ # Public: stop the application.
43
+ #
44
+ # Sends a SIGTERM to the app's process, and expects it to exit like a sane
45
+ # and well-behaved application within 30 seconds before sending a SIGKILL.
46
+ def stop
47
+ return unless @pid
48
+
49
+ info "stopping #{@pid}"
50
+ @master.close unless @master.closed?
51
+ debug "kill -TERM #{@pid}"
52
+ ::Process.kill "TERM", -@pid
53
+
54
+ kill = after 30 do
55
+ if @running
56
+ warn "process #{@pid} did not shut down cleanly, killing it"
57
+ debug "kill -KILL #{@pid}"
58
+ ::Process.kill "KILL", -@pid
59
+ end
60
+ end
61
+
62
+ wait :running # wait for the exit callback
63
+ kill.cancel
64
+
65
+ rescue Errno::ESRCH, Errno::EPERM
66
+ exited
67
+ end
68
+
69
+ # Internal: callback for when the application port has opened
70
+ def port_opened
71
+ info "listening on #{config.host}:#{config.app_port}"
72
+ signal :running, true
73
+ end
74
+
75
+ # Internal: callback for when the process has exited.
76
+ def exited
77
+ debug "pid #{@pid} has exited"
78
+ info "exited."
79
+ @running = false
80
+ @pid = nil
81
+ @wait_for_port.terminate if @wait_for_port.alive?
82
+ @wait_for_exit.terminate if @wait_for_exit.alive?
83
+ signal :running, false
84
+ end
85
+
86
+ # Private: start the application process and associated infrastructure
87
+ #
88
+ # Intended to be synchronous, as it blocks until the app has started (or
89
+ # failed to start).
90
+ #
91
+ # Returns true if the app started, false if not.
92
+ def start_app
93
+ @starting = true
94
+ info "starting process `#{config.command}`"
95
+
96
+ # give the child process a terminal so output isn't buffered
97
+ @master, slave = PTY.open
98
+ @pid = ::Process.spawn %Q(bash -c "#{config.command}"),
99
+ :in => slave,
100
+ :out => slave,
101
+ :err => slave,
102
+ :chdir => config.dir,
103
+ :pgroup => true
104
+ slave.close
105
+ proxy_output @master
106
+ debug "started with pid #{@pid}"
107
+
108
+ @wait_for_exit = WaitForExit.new @pid, Actor.current
109
+ @wait_for_port = WaitForPort.new config.host, config.app_port, Actor.current
110
+
111
+ timer = after config.startup_timeout do
112
+ warn "application startup took more than #{config.startup_timeout}"
113
+ stop!
114
+ end
115
+
116
+ @running = wait :running
117
+
118
+ @starting = false
119
+ timer.cancel
120
+
121
+ @running
122
+ rescue Errno::ENOENT
123
+ @starting = false
124
+ @running = false
125
+ false
126
+ ensure
127
+ unless @running
128
+ warn "could not start `#{config.command}`"
129
+ end
130
+ end
131
+
132
+ # Private: proxy output streams to the logger.
133
+ #
134
+ # Fire and forget, runs in its own thread.
135
+ def proxy_output(input)
136
+ Thread.new do
137
+ until input.eof?
138
+ info input.gets.strip
139
+ end
140
+ end
141
+ end
142
+
143
+ end
144
+
145
+ end
@@ -0,0 +1,116 @@
1
+ module Ringleader
2
+ class Server < Reel::Server
3
+ include Celluloid::IO # hurk
4
+ include Celluloid::Logger
5
+
6
+ ASSET_PATH = Pathname.new(File.expand_path("../../../assets", __FILE__))
7
+ ACTIONS = %w(enable disable stop start restart).freeze
8
+
9
+ def initialize(controller, host, port)
10
+ debug "starting webserver on #{host}:#{port}"
11
+ super host, port, &method(:on_connection)
12
+ @controller = controller
13
+ info "web control panel started on http://#{host}:#{port}"
14
+ end
15
+
16
+ def on_connection(connection)
17
+ request = connection.request
18
+ route connection, request if request
19
+ end
20
+
21
+ # thanks to dcell explorer for this code
22
+ def route(connection, request)
23
+ if request.url == "/"
24
+ path = "index.html"
25
+ else
26
+ path = request.url[%r{^/([a-z0-9\.\-_]+(/[a-z0-9\.\-_]+)*)$}, 1]
27
+ end
28
+
29
+ if !path or path[".."]
30
+ connection.respond :not_found, "Not found"
31
+ debug "404 #{path}"
32
+ return
33
+ end
34
+
35
+ case request.method
36
+ when :get
37
+ if path == "apps"
38
+ app_index connection
39
+ elsif path =~ %r(^apps/\w+)
40
+ show_app path, request.body, connection
41
+ else
42
+ static_file path, connection
43
+ end
44
+ when :post
45
+ update_app path, request.body, connection
46
+ end
47
+ end
48
+
49
+ def app_index(connection)
50
+ json = @controller.apps.map { |app| app_as_json(app) }.to_json
51
+ connection.respond :ok, json
52
+ debug "GET /apps: 200"
53
+ end
54
+
55
+ def static_file(path, connection)
56
+ filename = ASSET_PATH + path
57
+ if filename.exist?
58
+ mime_type = content_type_for filename.extname
59
+ filename.open("r") do |file|
60
+ connection.respond :ok, {"Content-type" => mime_type}, file
61
+ end
62
+ debug "GET #{path}: 200"
63
+ else
64
+ connection.respond :not_found, "Not found"
65
+ debug "GET #{path}: 404"
66
+ end
67
+ end
68
+
69
+ def show_app(uri, body, connection)
70
+ _, name, _ = uri.split("/")
71
+ app = @controller.app name
72
+ if app
73
+ connection.respond :ok, app_as_json(app).to_json
74
+ debug "GET #{uri}: 200"
75
+ else
76
+ connection.respond :not_found, "Not found"
77
+ debug "GET #{uri}: 404"
78
+ end
79
+ end
80
+
81
+ def update_app(uri, body, connection)
82
+ _, name, action = uri.split("/")
83
+ app = @controller.app name
84
+ if app && ACTIONS.include?(action)
85
+ app.send action
86
+ connection.respond :ok, app_as_json(app).to_json
87
+ debug "POST #{uri}: 200"
88
+ else
89
+ connection.respond :not_found, "Not found"
90
+ debug "POST #{uri}: 404"
91
+ end
92
+ end
93
+
94
+ def app_as_json(app)
95
+ AppSerializer.new(app)
96
+ end
97
+
98
+ def content_type_for(extname)
99
+ case extname
100
+ when ".html"
101
+ "text/html"
102
+ when ".js"
103
+ "application/json"
104
+ when ".css"
105
+ "text/css"
106
+ when ".ico"
107
+ "image/x-icon"
108
+ when ".png"
109
+ "image/png"
110
+ else
111
+ "text/plain"
112
+ end
113
+ end
114
+
115
+ end
116
+ end
@@ -1,3 +1,3 @@
1
1
  module Ringleader
2
- VERSION = "0.0.2"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -8,7 +8,7 @@ module Ringleader
8
8
  end
9
9
 
10
10
  def wait
11
- Process.waitpid @pid
11
+ ::Process.waitpid @pid
12
12
  @app.exited!
13
13
  terminate
14
14
  end
@@ -18,6 +18,7 @@ Gem::Specification.new do |gem|
18
18
 
19
19
  gem.add_dependency "celluloid", "~> 0.11.0"
20
20
  gem.add_dependency "celluloid-io", "~> 0.11.0"
21
+ gem.add_dependency "reel", "~> 0.1.0"
21
22
  gem.add_dependency "trollop", "~> 1.16.2"
22
23
  gem.add_dependency "rainbow", "~> 1.1.4"
23
24
  gem.add_dependency "color", "~> 1.4.1"
Binary file
@@ -2,7 +2,7 @@
2
2
  main_site:
3
3
  dir: "~/apps/main"
4
4
  command: "bundle exec foreman start"
5
- hostname: "0.0.0.0"
5
+ host: "0.0.0.0"
6
6
  server_port: 3000
7
7
  app_port: 4000
8
8
  idle_timeout: 1800
@@ -0,0 +1,5 @@
1
+ ---
2
+ main_site:
3
+ dir: "~/apps/main"
4
+ command: "bundle exec foreman start"
5
+ server_port: 3000
@@ -0,0 +1,6 @@
1
+
2
+ ---
3
+ main_site:
4
+ dir: "~/apps/main"
5
+ command: "bundle exec foreman start"
6
+ app_port: 4000
@@ -0,0 +1,6 @@
1
+ ---
2
+ rvm_app:
3
+ dir: "~/apps/main"
4
+ rvm: "foreman start"
5
+ server_port: 3000
6
+ app_port: 4000
@@ -15,8 +15,8 @@ describe Ringleader::Config do
15
15
  expect(config.dir).to eq("~/apps/main")
16
16
  end
17
17
 
18
- it "includes a default hostname" do
19
- expect(subject.apps["admin"].hostname).to eq("127.0.0.1")
18
+ it "includes a default host" do
19
+ expect(subject.apps["admin"].host).to eq("127.0.0.1")
20
20
  end
21
21
 
22
22
  it "includes a default idle timeout" do
@@ -41,4 +41,27 @@ describe Ringleader::Config do
41
41
  end
42
42
  end
43
43
 
44
+ context "with a config without an app port" do
45
+ it "raises an exception" do
46
+ expect {
47
+ Ringleader::Config.new("spec/fixtures/no_app_port.yml").apps
48
+ }.to raise_error(/app_port/)
49
+ end
50
+ end
51
+
52
+ context "with a config without a server port" do
53
+ it "raises an exception" do
54
+ expect {
55
+ Ringleader::Config.new("spec/fixtures/no_server_port.yml").apps
56
+ }.to raise_error(/server_port/)
57
+ end
58
+ end
59
+
60
+ context "with a config with an 'rvm' key instead of a 'command'" do
61
+ it "replaces the rvm command with a command to use rvm" do
62
+ config = Ringleader::Config.new "spec/fixtures/rvm.yml"
63
+ expect(config.apps["rvm_app"].command).to match(/rvm.*rvmrc.*exec.*foreman/)
64
+ end
65
+ end
66
+
44
67
  end