foreman_god 0.0.1

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 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: