foreman_god 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.iml
19
+ *.ipr
20
+ *.iws
21
+ .rakeTasks
22
+ *.log
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ bundler_args: --without development
3
+ script: "bundle exec rspec"
4
+ env:
5
+ - CI=true
6
+ rvm:
7
+ - 1.9.2
8
+ - 1.9.3
9
+ notifications:
10
+ recipients:
11
+ - ralf@embarkmobile.com
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in foreman_god.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'rspec'
8
+ gem 'capistrano'
9
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Embark Mobile
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # ForemanGod
2
+
3
+ God configuration with Procfiles.
4
+
5
+ ## Installation
6
+
7
+ $ gem install foreman_god
8
+
9
+ ## Usage
10
+
11
+ To run the sample:
12
+
13
+ god -D -c sample.god
14
+
15
+ Or with your own Procfile, add the following to your god configuration file:
16
+
17
+ require 'foreman_god'
18
+
19
+ ForemanGod.watch File.dirname(__FILE__) # Or an absolute path to the folder containing the Procfile
20
+
21
+ To set environment variables, add an .env file in next to the Procfile.
22
+
23
+ To specify foreman options, add a .foreman file next to the Procfile.
24
+
25
+ See samples/configuration for a complete example.
26
+
27
+ ## Contributing
28
+
29
+ 1. Fork it
30
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
31
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
32
+ 4. Push to the branch (`git push origin my-new-feature`)
33
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'foreman_god/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "foreman_god"
8
+ gem.version = ForemanGod::VERSION
9
+ gem.authors = ["Ralf Kistner"]
10
+ gem.email = ["ralf@embarkmobile.com"]
11
+ gem.description = %q{God configuration with Procfiles}
12
+ gem.summary = %q{Configure God using foreman-style Procfiles.}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency 'foreman'
21
+ gem.add_dependency 'god'
22
+ end
@@ -0,0 +1,25 @@
1
+ require "foreman_god/version"
2
+ require "foreman_god/god_config"
3
+
4
+ module ForemanGod
5
+
6
+ # ForemanGod.watch File.dirname(__FILE__) => calls God.watch with the Procfile in the current file's folder
7
+ # ForemanGod.watch '/var/www/*/current/' => watch all folders matching the glob which contain either a .foreman file or a
8
+ # Procfile.
9
+ def self.watch glob
10
+ # We append a backslash so that only folders are matched
11
+ glob += '/' unless glob.end_with? '/'
12
+ Dir[glob].each do |d|
13
+ if File.exists?(File.join(d, 'Procfile')) || File.exists?(File.join(d, '.foreman'))
14
+ watch_dir d
15
+ end
16
+ end
17
+ end
18
+
19
+ def self.watch_dir dir
20
+ config = GodConfig.new(dir)
21
+ config.watch
22
+ config
23
+ end
24
+ end
25
+
@@ -0,0 +1,157 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+ require 'foreman'
4
+ require 'foreman/engine'
5
+ require 'thor/core_ext/hash_with_indifferent_access'
6
+ require 'god'
7
+ require 'foreman_god'
8
+
9
+ module God
10
+ module Conditions
11
+ # Adapted from https://gist.github.com/571095
12
+ class ForemanRestartFileTouched < PollCondition
13
+ attr_accessor :restart_file
14
+
15
+ def initialize
16
+ super
17
+ end
18
+
19
+ def process_start_time
20
+ Time.parse(`ps -o lstart= -p #{self.watch.pid}`)
21
+ end
22
+
23
+ def restart_file_modification_time
24
+ File.mtime(self.restart_file) rescue Time.at(0)
25
+ end
26
+
27
+ def valid?
28
+ valid = true
29
+ valid &= complain("Attribute 'restart_file' must be specified", self) if self.restart_file.nil?
30
+ valid
31
+ end
32
+
33
+ def test
34
+ process_start_time < restart_file_modification_time
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+
41
+ module ForemanGod
42
+ class GodConfig
43
+ attr_reader :dir_name, :options, :engine
44
+
45
+ def initialize dir
46
+ @dir_name = File.basename(File.absolute_path(dir))
47
+
48
+ options_file = File.join(dir, ".foreman")
49
+ temp_options = {}
50
+ temp_options = (::YAML::load_file(options_file) || {}) if File.exists? options_file
51
+ @options = Thor::CoreExt::HashWithIndifferentAccess.new(temp_options)
52
+
53
+
54
+ @engine = Foreman::Engine.new(@options)
55
+
56
+ if @options[:env]
57
+ @options[:env].split(",").each do |file|
58
+ @engine.load_env file
59
+ end
60
+ else
61
+ default_env = File.join(dir, ".env")
62
+ @engine.load_env default_env if File.exists?(default_env)
63
+ end
64
+
65
+ @engine.load_procfile(File.join(dir, "Procfile"))
66
+ end
67
+
68
+ def app_name
69
+ @options[:app] || @dir_name
70
+ end
71
+
72
+ def user_name
73
+ @options[:user]
74
+ end
75
+
76
+ def log_path
77
+ @options[:log] || 'log'
78
+ end
79
+
80
+ def watch_process(name, process, n)
81
+ port = @engine.port_for(process, n)
82
+ base_env = process.instance_variable_get(:@options)[:env]
83
+ env = base_env.merge({'PORT' => port.to_s})
84
+
85
+ God.watch do |w|
86
+ w.dir = process.cwd
87
+ w.name = "#{app_name}-#{name}-#{n}"
88
+ w.group = app_name
89
+ w.interval = 60.seconds
90
+ w.env = env
91
+ w.start = process.expanded_command(env)
92
+ log = File.expand_path(log_path, process.cwd)
93
+ if File.directory? log
94
+ w.log = File.join(log, "#{app_name}-#{name}-#{n}.log")
95
+ else
96
+ LOG.warn "Log path does not exist: #{log}"
97
+ end
98
+
99
+ w.uid = user_name if user_name
100
+ # w.gid = ?
101
+
102
+ w.transition(:up, :restart) do |on|
103
+ on.condition(:memory_usage) do |c|
104
+ c.above = 350.megabytes
105
+ c.times = 2
106
+ end
107
+
108
+ on.condition(:foreman_restart_file_touched) do |c|
109
+ c.interval = 5.seconds
110
+ # Should we make this path configurable?
111
+ c.restart_file = File.join(process.cwd, 'tmp', 'restart.txt')
112
+ end
113
+ end
114
+
115
+ # determine the state on startup
116
+ w.transition(:init, { true => :up, false => :start }) do |on|
117
+ on.condition(:process_running) do |c|
118
+ c.running = true
119
+ end
120
+ end
121
+
122
+ # determine when process has finished starting
123
+ w.transition([:start, :restart], :up) do |on|
124
+ on.condition(:process_running) do |c|
125
+ c.running = true
126
+ c.interval = 5.seconds
127
+ end
128
+
129
+ # failsafe
130
+ on.condition(:tries) do |c|
131
+ c.times = 5
132
+ c.transition = :start
133
+ c.interval = 5.seconds
134
+ end
135
+ end
136
+
137
+ # start if process is not running
138
+ w.transition(:up, :start) do |on|
139
+ on.condition(:process_running) do |c|
140
+ c.running = false
141
+ end
142
+ end
143
+
144
+ end
145
+ end
146
+
147
+ def watch
148
+ @engine.each_process do |name, process|
149
+ 1.upto(@engine.formation[name]) do |n|
150
+ watch_process name, process, n
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+
@@ -0,0 +1,3 @@
1
+ module ForemanGod
2
+ VERSION = "0.0.1"
3
+ end
data/sample.god ADDED
@@ -0,0 +1,3 @@
1
+ require 'foreman_god'
2
+
3
+ ForemanGod.watch File.join(File.dirname(__FILE__), 'samples', 'simple')
@@ -0,0 +1,2 @@
1
+ MY_VAR=12345
2
+ ANOTHER_VAR=yes
@@ -0,0 +1,4 @@
1
+ formation: loop=1,another=2
2
+ app: configured-app
3
+ user: test
4
+ log: .
@@ -0,0 +1,2 @@
1
+ loop: ruby ../simple_loop.rb -p $PORT
2
+ another: ruby ../simple_loop.rb
@@ -0,0 +1 @@
1
+ loop: ruby ../simple_loop.rb -p $PORT
@@ -0,0 +1,8 @@
1
+ puts "Starting simple loop"
2
+ begin
3
+ while true
4
+ sleep 1
5
+ end
6
+ rescue Exception => e
7
+ puts "Terminated loop with #{e.inspect}"
8
+ end
@@ -0,0 +1,83 @@
1
+ require 'spec_helper'
2
+ require 'foreman_god/god_config'
3
+ require 'god'
4
+ require 'etc'
5
+
6
+ describe GodConfig do
7
+ context "simple" do
8
+ let(:config) { GodConfig.new(sample('simple')) }
9
+
10
+ it "should load basic properties" do
11
+ config.dir_name.should == 'simple'
12
+ config.app_name.should == 'simple'
13
+ config.user_name.should == nil
14
+ config.options.should == {}
15
+ end
16
+
17
+ it "should watch" do
18
+ config.watch
19
+ God.watches.values.count.should == 1
20
+ watch = God.watches.values.first
21
+ watch.should be
22
+
23
+ File.absolute_path(watch.dir).should == sample('simple')
24
+ watch.name.should == 'simple-loop-1'
25
+ watch.group.should == 'simple'
26
+ watch.interval.should == 60.seconds
27
+ watch.env.should == {'PORT' => '5000'}
28
+ watch.start.should == 'ruby ../simple_loop.rb -p 5000'
29
+ watch.log.should == '/dev/null'
30
+ watch.uid.should == nil
31
+
32
+ end
33
+
34
+ it "should log if log is specified" do
35
+ begin
36
+ FileUtils.mkdir 'samples/simple/log'
37
+ config.watch
38
+ watch = God.watches.values.first
39
+ watch.log.should == File.absolute_path('samples/simple/log/simple-loop-1.log')
40
+ ensure
41
+ FileUtils.rm_rf 'samples/simple/log'
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ context "configuration" do
48
+ let(:config) { GodConfig.new(sample('configuration')) }
49
+ let(:user) { Etc.getlogin }
50
+
51
+ it "should load basic properties" do
52
+ config.dir_name.should == 'configuration'
53
+ config.app_name.should == 'configured-app'
54
+ config.user_name.should == 'test'
55
+ config.options.should == {"formation"=>"loop=1,another=2", "app"=>"configured-app", "user"=>"test", "log" => "."}
56
+ end
57
+
58
+ it "should watch" do
59
+ config.options["user"] = user # We need to override the user to the current user, otherwise god will fail
60
+
61
+ config.watch
62
+ God.watches.values.count.should == 3
63
+ watch = God.watches.values.first
64
+ watch.should be
65
+
66
+ File.absolute_path(watch.dir).should == sample('configuration')
67
+ watch.name.should == 'configured-app-loop-1'
68
+ watch.group.should == 'configured-app'
69
+ watch.interval.should == 60.seconds
70
+ watch.env.should == {'PORT' => '5000', 'MY_VAR' => '12345', 'ANOTHER_VAR' => 'yes'}
71
+ watch.start.should == 'ruby ../simple_loop.rb -p 5000'
72
+ watch.log.should == File.absolute_path('samples/configuration/configured-app-loop-1.log')
73
+ watch.uid.should == user
74
+ end
75
+ end
76
+
77
+ it "should glob" do
78
+ # This should match spec/simple
79
+ ForemanGod.watch 'spec/*'
80
+ God.watches.values.count.should == 1
81
+ end
82
+
83
+ end
@@ -0,0 +1,35 @@
1
+ # Run god and test it
2
+ require 'spec_helper'
3
+
4
+ OPTIONS = "-p 13985 -P spec/tmp/god.pid -l #{LOG_FILE} --no-syslog"
5
+
6
+ describe "god" do
7
+ after do
8
+ `god #{OPTIONS} terminate`
9
+ FileUtils.rm_rf 'spec/simple/tmp'
10
+ end
11
+
12
+ it "should run and terminate a script" do
13
+ `god #{OPTIONS} --attach #{Process.pid} -c spec/simple/simple.god`
14
+ sleep 1
15
+ `god #{OPTIONS} terminate`
16
+
17
+ File.read('spec/tmp/test-simple-loop-1.log').should == "Starting simple loop\nTerminated loop with #<SignalException: SIGTERM>\n"
18
+ end
19
+
20
+ it "should restart a script on file touch" do
21
+ `god #{OPTIONS} --attach #{Process.pid} -c spec/simple/simple.god`
22
+
23
+ sleep 1
24
+
25
+ FileUtils.mkdir_p 'spec/simple/tmp'
26
+ FileUtils.touch 'spec/simple/tmp/restart.txt'
27
+
28
+ sleep 6 # Currently the file is checked every 5 seconds
29
+
30
+ `god #{OPTIONS} terminate`
31
+
32
+ File.read('spec/tmp/test-simple-loop-1.log').should == "Starting simple loop\nTerminated loop with #<SignalException: SIGTERM>\n"*2
33
+ end
34
+ end
35
+
@@ -0,0 +1,2 @@
1
+ app: test-simple
2
+ log: ../tmp
@@ -0,0 +1 @@
1
+ loop: ruby simple_loop.rb -p $PORT
@@ -0,0 +1,3 @@
1
+ require 'foreman_god'
2
+
3
+ ForemanGod.watch File.dirname(__FILE__)
@@ -0,0 +1,8 @@
1
+ puts "Starting simple loop"
2
+ begin
3
+ while true
4
+ sleep 1
5
+ end
6
+ rescue Exception => e
7
+ puts "Terminated loop with #{e.inspect}"
8
+ end
@@ -0,0 +1,47 @@
1
+ require 'rspec'
2
+ $load_god = true
3
+ require 'god'
4
+ require 'foreman_god'
5
+
6
+ LOG_FILE = 'god.log'
7
+ FileUtils.rm_f LOG_FILE
8
+
9
+ Object.send(:remove_const, :LOG)
10
+ LOG = God::Logger.new(File.open(LOG_FILE, 'w'))
11
+
12
+ include ForemanGod
13
+
14
+ def sample(name)
15
+ File.absolute_path(File.join(File.dirname(__FILE__), '..', 'samples', name))
16
+ end
17
+
18
+
19
+ module God
20
+
21
+ def self.reset
22
+ self.watches = nil
23
+ self.groups = nil
24
+ self.server = nil
25
+ self.inited = nil
26
+ self.host = nil
27
+ self.port = nil
28
+ self.pid_file_directory = File.join(File.dirname(__FILE__), 'tmp')
29
+ self.registry.reset
30
+ end
31
+ end
32
+
33
+
34
+ RSpec.configure do |config|
35
+ #Other config stuff goes here
36
+
37
+ # Clean/Reset God's state prior to running the tests
38
+ config.before :each do
39
+ God.reset
40
+ FileUtils.rm_rf 'spec/tmp'
41
+ FileUtils.mkdir_p 'spec/tmp'
42
+ end
43
+
44
+ config.after :each do
45
+ FileUtils.rm_rf 'spec/tmp'
46
+ end
47
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: foreman_god
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ralf Kistner
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: foreman
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: god
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: God configuration with Procfiles
47
+ email:
48
+ - ralf@embarkmobile.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - .travis.yml
55
+ - Gemfile
56
+ - LICENSE.txt
57
+ - README.md
58
+ - Rakefile
59
+ - foreman_god.gemspec
60
+ - lib/foreman_god.rb
61
+ - lib/foreman_god/god_config.rb
62
+ - lib/foreman_god/version.rb
63
+ - sample.god
64
+ - samples/configuration/.env
65
+ - samples/configuration/.foreman
66
+ - samples/configuration/Procfile
67
+ - samples/simple/Procfile
68
+ - samples/simple_loop.rb
69
+ - spec/god_config_spec.rb
70
+ - spec/integration_spec.rb
71
+ - spec/simple/.foreman
72
+ - spec/simple/Procfile
73
+ - spec/simple/simple.god
74
+ - spec/simple/simple_loop.rb
75
+ - spec/spec_helper.rb
76
+ homepage: ''
77
+ licenses: []
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 1.8.24
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: Configure God using foreman-style Procfiles.
100
+ test_files:
101
+ - spec/god_config_spec.rb
102
+ - spec/integration_spec.rb
103
+ - spec/simple/.foreman
104
+ - spec/simple/Procfile
105
+ - spec/simple/simple.god
106
+ - spec/simple/simple_loop.rb
107
+ - spec/spec_helper.rb
108
+ has_rdoc: