diff --git a/Gemfile b/Gemfile index 28cb64a..e0c2716 100644 --- a/Gemfile +++ b/Gemfile @@ -18,6 +18,7 @@ group :development do gem 'rcov', '~> 0.9.8' gem 'rr', '~> 1.0.2' gem 'rspec', '~> 2.0' + gem 'ZenTest' gem 'aws-s3' gem "rubyzip" end diff --git a/Gemfile.lock b/Gemfile.lock index 00dd40c..a63ebb6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,6 +8,7 @@ PATH GEM remote: http://rubygems.org/ specs: + ZenTest (4.6.2) aws-s3 (0.6.2) builder mime-types @@ -56,6 +57,7 @@ PLATFORMS x86-mingw32 DEPENDENCIES + ZenTest aws-s3 fakefs (~> 0.3.2) foreman! diff --git a/data/export/bluepill/master.pill.erb b/data/export/bluepill/master.pill.erb index 2b56d9d..69da4d7 100644 --- a/data/export/bluepill/master.pill.erb +++ b/data/export/bluepill/master.pill.erb @@ -5,7 +5,7 @@ Bluepill.application("<%= app %>", :foreground => false, :log_file => "/var/log/ <% engine.procfile.entries.each do |process| %> <% 1.upto(concurrency[process.name]) do |num| %> -<% port = engine.port_for(process, num, options[:port]) %> +<% port = engine.port_for(process, num, self.port) %> app.process("<%= process.name %>-<%=num%>") do |process| process.start_command = "<%= process.command.gsub("$PORT", port.to_s) %>" diff --git a/lib/foreman/cli.rb b/lib/foreman/cli.rb index 45f01aa..7dc1f3c 100644 --- a/lib/foreman/cli.rb +++ b/lib/foreman/cli.rb @@ -1,10 +1,14 @@ require "foreman" +require "foreman/helpers" require "foreman/engine" require "foreman/export" require "thor" require "yaml" class Foreman::CLI < Thor + include Foreman::Helpers + + class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile" desc "start", "Start the application" @@ -42,16 +46,17 @@ class Foreman::CLI < Thor def export(format, location=nil) check_procfile! - formatter = case format - when "inittab" then Foreman::Export::Inittab - when "upstart" then Foreman::Export::Upstart - when "bluepill" then Foreman::Export::Bluepill - when "runit" then Foreman::Export::Runit - else error "Unknown export format: #{format}." + begin + require "foreman/export/#{ format.tr('-', '_') }" + classy_format = classify(format) + formatter = constantize("Foreman::Export::#{ classy_format }") + rescue NameError => ex + error "Unknown export format: #{format} (no class Foreman::Export::#{ classy_format })." + rescue LoadError => ex + error "Unknown export format: #{format} (unable to load file 'foreman/export/#{ format.tr('-', '_') }')." end - formatter.new(engine).export(location, options) - + formatter.new(location, engine, options).export rescue Foreman::Export::Exception => ex error ex.message end diff --git a/lib/foreman/export/base.rb b/lib/foreman/export/base.rb index 7bebd43..c17f9d9 100644 --- a/lib/foreman/export/base.rb +++ b/lib/foreman/export/base.rb @@ -3,10 +3,17 @@ require "foreman/utils" class Foreman::Export::Base - attr_reader :engine + attr_reader :location, :engine, :app, :log, :port, :user, :template, :concurrency - def initialize(engine) - @engine = engine + def initialize(location, engine, options={}) + @location = location + @engine = engine + @app = options[:app] + @log = options[:log] + @port = options[:port] + @user = options[:user] + @template = options[:template] + @concurrency = Foreman::Utils.parse_concurrency(options[:concurrency]) end def export diff --git a/lib/foreman/export/bluepill.rb b/lib/foreman/export/bluepill.rb index fae8a8b..b72da2e 100644 --- a/lib/foreman/export/bluepill.rb +++ b/lib/foreman/export/bluepill.rb @@ -3,23 +3,21 @@ require "foreman/export" class Foreman::Export::Bluepill < Foreman::Export::Base - def export(location, options={}) + def export error("Must specify a location") unless location FileUtils.mkdir_p location - app = options[:app] || File.basename(engine.directory) - user = options[:user] || app - log_root = options[:log] || "/var/log/#{app}" - template_root = options[:template] + app = self.app || File.basename(engine.directory) + user = self.user || app + log_root = self.log || "/var/log/#{app}" + template_root = self.template Dir["#{location}/#{app}.pill"].each do |file| say "cleaning up: #{file}" FileUtils.rm(file) end - concurrency = Foreman::Utils.parse_concurrency(options[:concurrency]) - master_template = export_template("bluepill", "master.pill.erb", template_root) master_config = ERB.new(master_template).result(binding) write_file "#{location}/#{app}.pill", master_config diff --git a/lib/foreman/export/inittab.rb b/lib/foreman/export/inittab.rb index e8fb7a8..de04e30 100644 --- a/lib/foreman/export/inittab.rb +++ b/lib/foreman/export/inittab.rb @@ -2,20 +2,18 @@ require "foreman/export" class Foreman::Export::Inittab < Foreman::Export::Base - def export(fname=nil, options={}) - app = options[:app] || File.basename(engine.directory) - user = options[:user] || app - log_root = options[:log] || "/var/log/#{app}" - - concurrency = Foreman::Utils.parse_concurrency(options[:concurrency]) + def export + app = self.app || File.basename(engine.directory) + user = self.user || app + log_root = self.log || "/var/log/#{app}" inittab = [] inittab << "# ----- foreman #{app} processes -----" engine.procfile.entries.inject(1) do |index, process| - 1.upto(concurrency[process.name]) do |num| + 1.upto(self.concurrency[process.name]) do |num| id = app.slice(0, 2).upcase + sprintf("%02d", index) - port = engine.port_for(process, num, options[:port]) + port = engine.port_for(process, num, self.port) inittab << "#{id}:4:respawn:/bin/su - #{user} -c 'PORT=#{port} #{process.command} >> #{log_root}/#{process.name}-#{num}.log 2>&1'" index += 1 end diff --git a/lib/foreman/export/runit.rb b/lib/foreman/export/runit.rb index b3c74cb..b71b279 100644 --- a/lib/foreman/export/runit.rb +++ b/lib/foreman/export/runit.rb @@ -4,21 +4,19 @@ require "foreman/export" class Foreman::Export::Runit < Foreman::Export::Base ENV_VARIABLE_REGEX = /([a-zA-Z_]+[a-zA-Z0-9_]*)=(\S+)/ - def export(location, options={}) + def export error("Must specify a location") unless location - app = options[:app] || File.basename(engine.directory) - user = options[:user] || app - log_root = options[:log] || "/var/log/#{app}" - template_root = options[:template] - - concurrency = Foreman::Utils.parse_concurrency(options[:concurrency]) + app = self.app || File.basename(engine.directory) + user = self.user || app + log_root = self.log || "/var/log/#{app}" + template_root = self.template run_template = export_template('runit', 'run.erb', template_root) log_run_template = export_template('runit', 'log_run.erb', template_root) engine.procfile.entries.each do |process| - 1.upto(concurrency[process.name]) do |num| + 1.upto(self.concurrency[process.name]) do |num| process_directory = "#{location}/#{app}-#{process.name}-#{num}" process_env_directory = "#{process_directory}/env" process_log_directory = "#{process_directory}/log" @@ -31,7 +29,7 @@ class Foreman::Export::Runit < Foreman::Export::Base write_file "#{process_directory}/run", run FileUtils.chmod 0755, "#{process_directory}/run" - port = engine.port_for(process, num, options[:port]) + port = engine.port_for(process, num, self.port) environment_variables = {'PORT' => port}. merge(engine.environment). merge(inline_variables(process.command)) diff --git a/lib/foreman/export/upstart.rb b/lib/foreman/export/upstart.rb index 9ae9717..e3ba2d4 100644 --- a/lib/foreman/export/upstart.rb +++ b/lib/foreman/export/upstart.rb @@ -3,23 +3,21 @@ require "foreman/export" class Foreman::Export::Upstart < Foreman::Export::Base - def export(location, options={}) + def export error("Must specify a location") unless location FileUtils.mkdir_p location - app = options[:app] || File.basename(engine.directory) - user = options[:user] || app - log_root = options[:log] || "/var/log/#{app}" - template_root = options[:template] + app = self.app || File.basename(engine.directory) + user = self.user || app + log_root = self.log || "/var/log/#{app}" + template_root = self.template Dir["#{location}/#{app}*.conf"].each do |file| say "cleaning up: #{file}" FileUtils.rm(file) end - concurrency = Foreman::Utils.parse_concurrency(options[:concurrency]) - master_template = export_template("upstart", "master.conf.erb", template_root) master_config = ERB.new(master_template).result(binding) write_file "#{location}/#{app}.conf", master_config @@ -27,13 +25,13 @@ class Foreman::Export::Upstart < Foreman::Export::Base process_template = export_template("upstart", "process.conf.erb", template_root) engine.procfile.entries.each do |process| - next if (conc = concurrency[process.name]) < 1 + next if (conc = self.concurrency[process.name]) < 1 process_master_template = export_template("upstart", "process_master.conf.erb", template_root) process_master_config = ERB.new(process_master_template).result(binding) write_file "#{location}/#{app}-#{process.name}.conf", process_master_config - 1.upto(concurrency[process.name]) do |num| - port = engine.port_for(process, num, options[:port]) + 1.upto(self.concurrency[process.name]) do |num| + port = engine.port_for(process, num, self.port) process_config = ERB.new(process_template).result(binding) write_file "#{location}/#{app}-#{process.name}-#{num}.conf", process_config end diff --git a/lib/foreman/helpers.rb b/lib/foreman/helpers.rb new file mode 100644 index 0000000..4b5625d --- /dev/null +++ b/lib/foreman/helpers.rb @@ -0,0 +1,50 @@ +module Foreman::Helpers + # Copied whole sale from, https://github.com/defunkt/resque/ + + # Given a word with dashes, returns a camel cased version of it. + # + # classify('job-name') # => 'JobName' + def classify(dashed_word) + dashed_word.split('-').each { |part| part[0] = part[0].chr.upcase }.join + end + + # Tries to find a constant with the name specified in the argument string: + # + # constantize("Module") # => Module + # constantize("Test::Unit") # => Test::Unit + # + # The name is assumed to be the one of a top-level constant, no matter + # whether it starts with "::" or not. No lexical context is taken into + # account: + # + # C = 'outside' + # module M + # C = 'inside' + # C # => 'inside' + # constantize("C") # => 'outside', same as ::C + # end + # + # NameError is raised when the constant is unknown. + def constantize(camel_cased_word) + camel_cased_word = camel_cased_word.to_s + + if camel_cased_word.include?('-') + camel_cased_word = classify(camel_cased_word) + end + + names = camel_cased_word.split('::') + names.shift if names.empty? || names.first.empty? + + constant = Object + names.each do |name| + args = Module.method(:const_get).arity != 1 ? [false] : [] + + if constant.const_defined?(name, *args) + constant = constant.const_get(name) + else + constant = constant.const_missing(name) + end + end + constant + end +end \ No newline at end of file diff --git a/spec/foreman/cli_spec.rb b/spec/foreman/cli_spec.rb index 4170af9..430fa84 100644 --- a/spec/foreman/cli_spec.rb +++ b/spec/foreman/cli_spec.rb @@ -30,7 +30,9 @@ describe "Foreman::CLI", :fakefs do it "respects --env" do write_procfile write_env("envfile") - mock.instance_of(Foreman::Export::Upstart).export("/upstart", { "env" => "envfile" }) + mock_export = mock(Foreman::Export::Upstart) + mock(Foreman::Export::Upstart).new("/upstart", is_a(Foreman::Engine), { "env" => "envfile" }) { mock_export } + mock_export.export foreman %{ export upstart /upstart --env envfile } end end @@ -49,7 +51,7 @@ describe "Foreman::CLI", :fakefs do describe "with an invalid formatter" do it "prints an error" do - mock_error(subject, "Unknown export format: invalidformatter.") do + mock_error(subject, "Unknown export format: invalidformatter (unable to load file 'foreman/export/invalidformatter').") do subject.export("invalidformatter") end end @@ -60,7 +62,9 @@ describe "Foreman::CLI", :fakefs do it "runs successfully" do dont_allow(subject).error - mock.instance_of(Foreman::Export::Upstart).export("/tmp/foo", {}) + mock_export = mock(Foreman::Export::Upstart) + mock(Foreman::Export::Upstart).new("/tmp/foo", is_a(Foreman::Engine), {}) { mock_export } + mock_export.export subject.export("upstart", "/tmp/foo") end end diff --git a/spec/foreman/export/bluepill_spec.rb b/spec/foreman/export/bluepill_spec.rb index 5d2eefa..7ced939 100644 --- a/spec/foreman/export/bluepill_spec.rb +++ b/spec/foreman/export/bluepill_spec.rb @@ -5,20 +5,25 @@ require "tmpdir" describe Foreman::Export::Bluepill, :fakefs do let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } - let(:engine) { Foreman::Engine.new(procfile) } - let(:bluepill) { Foreman::Export::Bluepill.new(engine) } + let(:engine) { Foreman::Engine.new(procfile) } + let(:options) { Hash.new } + let(:bluepill) { Foreman::Export::Bluepill.new("/tmp/init", engine, options) } before(:each) { load_export_templates_into_fakefs("bluepill") } before(:each) { stub(bluepill).say } it "exports to the filesystem" do - bluepill.export("/tmp/init") + bluepill.export normalize_space(File.read("/tmp/init/app.pill")).should == normalize_space(example_export_file("bluepill/app.pill")) end - it "exports to the filesystem with concurrency" do - bluepill.export("/tmp/init", :concurrency => "alpha=2") + context "with concurrency" do + let(:options) { Hash[:concurrency => "alpha=2"] } - normalize_space(File.read("/tmp/init/app.pill")).should == normalize_space(example_export_file("bluepill/app-concurrency.pill")) + it "exports to the filesystem with concurrency" do + bluepill.export + normalize_space(File.read("/tmp/init/app.pill")).should == normalize_space(example_export_file("bluepill/app-concurrency.pill")) + end end + end diff --git a/spec/foreman/export/runit_spec.rb b/spec/foreman/export/runit_spec.rb index 002a51e..6d04ae6 100644 --- a/spec/foreman/export/runit_spec.rb +++ b/spec/foreman/export/runit_spec.rb @@ -6,7 +6,7 @@ require "tmpdir" describe Foreman::Export::Runit, :fakefs do let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile", 'bar=baz') } let(:engine) { Foreman::Engine.new(procfile) } - let(:runit) { Foreman::Export::Runit.new(engine) } + let(:runit) { Foreman::Export::Runit.new('/tmp/init', engine, :concurrency => 'alpha=2,bravo=1') } before(:each) { load_export_templates_into_fakefs("runit") } before(:each) { stub(runit).say } @@ -14,7 +14,8 @@ describe Foreman::Export::Runit, :fakefs do it "exports to the filesystem" do FileUtils.mkdir_p('/tmp/init') - runit.export('/tmp/init', :concurrency => "alpha=2,bravo=1") + + runit.export File.read("/tmp/init/app-alpha-1/run").should == example_export_file('runit/app-alpha-1-run') File.read("/tmp/init/app-alpha-1/log/run").should == @@ -35,6 +36,6 @@ describe Foreman::Export::Runit, :fakefs do end it "creates a full path to the export directory" do - expect { runit.export('/tmp/init', :concurrency => "alpha=2,bravo=1") }.to_not raise_error(Errno::ENOENT) + expect { runit.export }.to_not raise_error(Errno::ENOENT) end end diff --git a/spec/foreman/export/upstart_spec.rb b/spec/foreman/export/upstart_spec.rb index 39881ca..fb7b5d4 100644 --- a/spec/foreman/export/upstart_spec.rb +++ b/spec/foreman/export/upstart_spec.rb @@ -5,14 +5,15 @@ require "tmpdir" describe Foreman::Export::Upstart, :fakefs do let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } - let(:engine) { Foreman::Engine.new(procfile) } - let(:upstart) { Foreman::Export::Upstart.new(engine) } + let(:engine) { Foreman::Engine.new(procfile) } + let(:options) { Hash.new } + let(:upstart) { Foreman::Export::Upstart.new("/tmp/init", engine, options) } before(:each) { load_export_templates_into_fakefs("upstart") } before(:each) { stub(upstart).say } it "exports to the filesystem" do - upstart.export("/tmp/init") + upstart.export File.read("/tmp/init/app.conf").should == example_export_file("upstart/app.conf") File.read("/tmp/init/app-alpha.conf").should == example_export_file("upstart/app-alpha.conf") @@ -21,18 +22,23 @@ describe Foreman::Export::Upstart, :fakefs do File.read("/tmp/init/app-bravo-1.conf").should == example_export_file("upstart/app-bravo-1.conf") end - it "exports to the filesystem with concurrency" do - upstart.export("/tmp/init", :concurrency => "alpha=2") + context "with concurrency" do + let(:options) { Hash[:concurrency => "alpha=2"] } - File.read("/tmp/init/app.conf").should == example_export_file("upstart/app.conf") - File.read("/tmp/init/app-alpha.conf").should == example_export_file("upstart/app-alpha.conf") - File.read("/tmp/init/app-alpha-1.conf").should == example_export_file("upstart/app-alpha-1.conf") - File.read("/tmp/init/app-alpha-2.conf").should == example_export_file("upstart/app-alpha-2.conf") - File.exists?("/tmp/init/app-bravo-1.conf").should == false + it "exports to the filesystem with concurrency" do + upstart.export + + File.read("/tmp/init/app.conf").should == example_export_file("upstart/app.conf") + File.read("/tmp/init/app-alpha.conf").should == example_export_file("upstart/app-alpha.conf") + File.read("/tmp/init/app-alpha-1.conf").should == example_export_file("upstart/app-alpha-1.conf") + File.read("/tmp/init/app-alpha-2.conf").should == example_export_file("upstart/app-alpha-2.conf") + File.exists?("/tmp/init/app-bravo-1.conf").should == false + end end context "with alternate templates" do let(:template_root) { "/tmp/alternate" } + let(:upstart) { Foreman::Export::Upstart.new("/tmp/init", engine, :template => template_root) } before do FileUtils.mkdir_p template_root @@ -40,7 +46,7 @@ describe Foreman::Export::Upstart, :fakefs do end it "can export with alternate template files" do - upstart.export("/tmp/init", :template => template_root) + upstart.export File.read("/tmp/init/app.conf").should == "alternate_template\n" end @@ -61,7 +67,7 @@ describe Foreman::Export::Upstart, :fakefs do end it "can export with alternate template files" do - upstart.export("/tmp/init") + upstart.export File.read("/tmp/init/app.conf").should == "default_alternate_template\n" end