Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Basic Procfile parsing.

  • Loading branch information...
commit 85c0dee592ff7f803f886794af2f47d64952ccae 0 parents
@rkistner rkistner authored
22 .gitignore
@@ -0,0 +1,22 @@
+*.gem
+*.rbc
+.bundle
+.config
+.yardoc
+Gemfile.lock
+InstalledFiles
+_yardoc
+coverage
+doc/
+lib/bundler/man
+pkg
+rdoc
+spec/reports
+test/tmp
+test/version_tmp
+tmp
+*.iml
+*.ipr
+*.iws
+.rakeTasks
+*.log
4 Gemfile
@@ -0,0 +1,4 @@
+source 'https://rubygems.org'
+
+# Specify your gem's dependencies in foreman_god.gemspec
+gemspec
22 LICENSE.txt
@@ -0,0 +1,22 @@
+Copyright (c) 2012 Embark Mobile
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
33 README.md
@@ -0,0 +1,33 @@
+# ForemanGod
+
+God configuration with Procfiles.
+
+## Installation
+
+ $ gem install foreman_god
+
+## Usage
+
+To run the sample:
+
+ god -D -c sample.god
+
+Or with your own Procfile, add the following to your god configuration file:
+
+ require 'foreman_god'
+
+ ForemanGod.watch File.dirname(__FILE__) # Or an absolute path to the folder containing the Procfile
+
+To set environment variables, add an .env file in next to the Procfile.
+
+To specify foreman options, add a .foreman file next to the Procfile.
+
+See samples/configuration for a complete example.
+
+## Contributing
+
+1. Fork it
+2. Create your feature branch (`git checkout -b my-new-feature`)
+3. Commit your changes (`git commit -am 'Add some feature'`)
+4. Push to the branch (`git push origin my-new-feature`)
+5. Create new Pull Request
1  Rakefile
@@ -0,0 +1 @@
+require "bundler/gem_tasks"
22 foreman_god.gemspec
@@ -0,0 +1,22 @@
+# -*- encoding: utf-8 -*-
+lib = File.expand_path('../lib', __FILE__)
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+require 'foreman_god/version'
+
+Gem::Specification.new do |gem|
+ gem.name = "foreman_god"
+ gem.version = ForemanGod::VERSION
+ gem.authors = ["Ralf Kistner"]
+ gem.email = ["ralf@embarkmobile.com"]
+ gem.description = %q{God configuration with Procfiles}
+ gem.summary = %q{Configure God using foreman-style Procfiles.}
+ gem.homepage = ""
+
+ gem.files = `git ls-files`.split($/)
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
+ gem.require_paths = ["lib"]
+
+ gem.add_dependency 'foreman'
+ gem.add_dependency 'god'
+end
12 lib/foreman_god.rb
@@ -0,0 +1,12 @@
+require "foreman_god/version"
+require "foreman_god/god_config"
+
+module ForemanGod
+ # ForemanGod.watch File.dirname(__FILE__) => calls God.watch with the Procfile in the current file's folder
+ def self.watch dir
+ config = GodConfig.new(dir)
+ config.watch
+ config
+ end
+end
+
109 lib/foreman_god/god_config.rb
@@ -0,0 +1,109 @@
+require 'pathname'
+require 'yaml'
+require 'foreman'
+require 'foreman/engine'
+require 'thor/core_ext/hash_with_indifferent_access'
+require 'god'
+
+module ForemanGod
+ class GodConfig
+ attr_reader :dir_name, :options, :engine
+
+ def initialize dir
+ @dir_name = File.basename(File.absolute_path(dir))
+
+ options_file = File.join(dir, ".foreman")
+ temp_options = {}
+ temp_options = (::YAML::load_file(options_file) || {}) if File.exists? options_file
+ @options = Thor::CoreExt::HashWithIndifferentAccess.new(temp_options)
+
+
+ @engine = Foreman::Engine.new(@options)
+
+ if @options[:env]
+ @options[:env].split(",").each do |file|
+ @engine.load_env file
+ end
+ else
+ default_env = File.join(dir, ".env")
+ @engine.load_env default_env if File.exists?(default_env)
+ end
+
+ @engine.load_procfile(File.join(dir, "Procfile"))
+ end
+
+ def app_name
+ @options[:app] || @dir_name
+ end
+
+ def user_name
+ @options[:user]
+ end
+
+ def watch_process(name, process, n)
+ port = @engine.port_for(process, n)
+ base_env = process.instance_variable_get(:@options)[:env]
+ env = base_env.merge({'PORT' => port.to_s})
+
+ God.watch do |w|
+ w.dir = process.cwd
+ w.name = "#{app_name}-#{name}-#{n}"
+ w.group = app_name
+ w.interval = 60.seconds
+ w.env = env
+ w.start = process.expanded_command(env)
+ w.log = "#{app_name}-#{name}-#{n}.log" # TODO: make this folder configurable
+
+ w.uid = user_name if user_name
+ # w.gid = ?
+
+ w.transition(:up, :restart) do |on|
+ on.condition(:memory_usage) do |c|
+ c.above = 350.megabytes
+ c.times = 2
+ end
+ end
+
+ # determine the state on startup
+ w.transition(:init, { true => :up, false => :start }) do |on|
+ on.condition(:process_running) do |c|
+ c.running = true
+ end
+ end
+
+ # determine when process has finished starting
+ w.transition([:start, :restart], :up) do |on|
+ on.condition(:process_running) do |c|
+ c.running = true
+ c.interval = 5.seconds
+ end
+
+ # failsafe
+ on.condition(:tries) do |c|
+ c.times = 5
+ c.transition = :start
+ c.interval = 5.seconds
+ end
+ end
+
+ # start if process is not running
+ w.transition(:up, :start) do |on|
+ on.condition(:process_running) do |c|
+ c.running = false
+ end
+ end
+
+ end
+ end
+
+ def watch
+ @engine.each_process do |name, process|
+ 1.upto(@engine.formation[name]) do |n|
+ watch_process name, process, n
+ end
+ end
+ end
+ end
+end
+
+
3  lib/foreman_god/version.rb
@@ -0,0 +1,3 @@
+module ForemanGod
+ VERSION = "0.0.1"
+end
3  sample.god
@@ -0,0 +1,3 @@
+require 'foreman_god'
+
+ForemanGod.watch File.join(File.dirname(__FILE__), 'samples', 'simple')
2  samples/configuration/.env
@@ -0,0 +1,2 @@
+MY_VAR=12345
+ANOTHER_VAR=yes
3  samples/configuration/.foreman
@@ -0,0 +1,3 @@
+formation: loop=1,another=2
+app: configured-app
+user: test
2  samples/configuration/Procfile
@@ -0,0 +1,2 @@
+loop: ruby ../simple_loop.rb -p $PORT
+another: ruby ../simple_loop.rb
1  samples/simple/Procfile
@@ -0,0 +1 @@
+loop: ruby ../simple_loop.rb -p $PORT
8 samples/simple_loop.rb
@@ -0,0 +1,8 @@
+puts "Starting simple loop"
+begin
+ while true
+ sleep 1
+ end
+rescue Exception => e
+ puts "Terminated loop with #{e.inspect}"
+end
68 spec/god_config_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+require 'foreman_god/god_config'
+require 'god'
+require 'etc'
+
+describe GodConfig do
+ context "simple" do
+ let(:config) { GodConfig.new(sample('simple')) }
+
+ it "should load basic properties" do
+ config.dir_name.should == 'simple'
+ config.app_name.should == 'simple'
+ config.user_name.should == nil
+ config.options.should == {}
+ end
+
+ it "should watch" do
+ config.watch
+ God.watches.values.count.should == 1
+ watch = God.watches.values.first
+ watch.should be
+
+ File.absolute_path(watch.dir).should == sample('simple')
+ watch.name.should == 'simple-loop-1'
+ watch.group.should == 'simple'
+ watch.interval.should == 60.seconds
+ watch.env.should == {'PORT' => '5000'}
+ watch.start.should == 'ruby ../simple_loop.rb -p 5000'
+ watch.log.should == 'simple-loop-1.log'
+ watch.uid.should == nil
+
+ end
+
+
+ end
+
+ context "configuration" do
+ let(:config) { GodConfig.new(sample('configuration')) }
+ let(:user) { Etc.getlogin }
+ it "should load basic properties" do
+ config.dir_name.should == 'configuration'
+ config.app_name.should == 'configured-app'
+ config.user_name.should == 'test'
+ config.options.should == {"formation"=>"loop=1,another=2", "app"=>"configured-app", "user"=>"test"}
+ end
+
+ it "should watch" do
+ config.options["user"] = user # We need to override the user to the current user, otherwise god will fail
+
+ config.watch
+ God.watches.values.count.should == 3
+ watch = God.watches.values.first
+ watch.should be
+
+ File.absolute_path(watch.dir).should == sample('configuration')
+ watch.name.should == 'configured-app-loop-1'
+ watch.group.should == 'configured-app'
+ watch.interval.should == 60.seconds
+ watch.env.should == {'PORT' => '5000', 'MY_VAR' => '12345', 'ANOTHER_VAR' => 'yes'}
+ watch.start.should == 'ruby ../simple_loop.rb -p 5000'
+ watch.log.should == 'configured-app-loop-1.log'
+ watch.uid.should == user
+ end
+
+
+ end
+
+end
34 spec/spec_helper.rb
@@ -0,0 +1,34 @@
+require 'rspec'
+$load_god = true
+require 'god'
+
+include ForemanGod
+
+def sample(name)
+ File.absolute_path(File.join(File.dirname(__FILE__), '..', 'samples', name))
+end
+
+
+module God
+
+ def self.reset
+ self.watches = nil
+ self.groups = nil
+ self.server = nil
+ self.inited = nil
+ self.host = nil
+ self.port = nil
+ self.pid_file_directory = File.join(File.dirname(__FILE__), 'tmp')
+ self.registry.reset
+ end
+end
+
+
+RSpec.configure do |config|
+ #Other config stuff goes here
+
+ # Clean/Reset God's state prior to running the tests
+ config.before :each do
+ God.reset
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.