kibo 0.1.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,8 +1,9 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- gem "heroku"
4
3
  gem "trollop"
4
+ gem "thor"
5
5
  gem 'rake'
6
+ gem 'netrc'
6
7
 
7
8
  group :development do
8
9
  gem 'ronn'
data/Gemfile.lock ADDED
@@ -0,0 +1,46 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.1.3)
5
+ fakefs (0.3.2)
6
+ hpricot (0.8.6)
7
+ multi_json (1.3.6)
8
+ mustache (0.99.4)
9
+ netrc (0.7.7)
10
+ rake (0.9.2.2)
11
+ rdiscount (1.6.8)
12
+ ronn (0.7.3)
13
+ hpricot (>= 0.8.2)
14
+ mustache (>= 0.7.0)
15
+ rdiscount (>= 1.5.8)
16
+ rr (1.0.4)
17
+ rspec (2.11.0)
18
+ rspec-core (~> 2.11.0)
19
+ rspec-expectations (~> 2.11.0)
20
+ rspec-mocks (~> 2.11.0)
21
+ rspec-core (2.11.1)
22
+ rspec-expectations (2.11.3)
23
+ diff-lcs (~> 1.1.3)
24
+ rspec-mocks (2.11.2)
25
+ simplecov (0.6.4)
26
+ multi_json (~> 1.0)
27
+ simplecov-html (~> 0.5.3)
28
+ simplecov-html (0.5.3)
29
+ thor (0.16.0)
30
+ timecop (0.5.0)
31
+ trollop (2.0)
32
+
33
+ PLATFORMS
34
+ ruby
35
+
36
+ DEPENDENCIES
37
+ fakefs (~> 0.3.2)
38
+ netrc
39
+ rake
40
+ ronn
41
+ rr (~> 1.0.2)
42
+ rspec (~> 2.0)
43
+ simplecov
44
+ thor
45
+ timecop
46
+ trollop
data/kibo.gemspec CHANGED
@@ -14,8 +14,8 @@ class Gem::Specification
14
14
  def source(*args); end
15
15
  def group(*args); end
16
16
 
17
- def gem(name, options = {})
18
- @scope.add_dependency(name)
17
+ def gem(name, *requirements)
18
+ @scope.add_dependency(name, *requirements)
19
19
  end
20
20
  end
21
21
 
@@ -1,53 +1,39 @@
1
1
  require "trollop"
2
+ require "kibo/version"
2
3
 
3
4
  module Kibo::CommandLine
4
- def self.method_missing(sym, *args, &block)
5
- if block_given? || !args.empty?
6
- super
7
- elsif options.key?(sym)
8
- options[sym]
9
- elsif (sym.to_s =~ /(.*)\?/) && options.key?($1.to_sym)
10
- !! options[$1.to_sym]
11
- else
12
- super
13
- end
14
- end
5
+ extend self
15
6
 
16
- def self.options
17
- parse unless @options
18
- @options
7
+ def options
8
+ parse; @options
19
9
  end
20
10
 
21
- def self.subcommand
22
- parse unless @options
23
- @subcommand
11
+ def subcommand
12
+ parse; @subcommand
24
13
  end
25
14
 
26
- def self.args
27
- parse unless @options
28
- @args
29
- end
30
-
31
- def self.parse_and_get(name)
32
- parse unless @options
33
- instance_variable_get "@#{name}"
15
+ def args
16
+ parse; @args
34
17
  end
35
18
 
36
- SUBCOMMANDS = %w(create deploy spinup spindown reconfigure)
19
+ private
37
20
 
38
- def self.parse
21
+ def parse
22
+ return if @options
23
+
24
+ usage = Kibo::Commands.commands.map do |subcommand|
25
+ next unless description = Kibo::Commands.descriptions[subcommand.to_s]
26
+ " kibo [options] %-30s ... %s" % [ subcommand, description ]
27
+ end.compact.join("\n")
28
+
39
29
  @options = Trollop::options do
40
- version "test 1.2.3 (c) 2008 William Morgan"
30
+ version "kibo #{Kibo::VERSION} (c) 2012 radiospiel"
41
31
  banner <<-EOS
42
- kibo is an awesome program that does something very, very important.
32
+ kibo manages multiple application roles on single heroku dynos.
43
33
 
44
34
  Usage:
45
35
 
46
- kibo [options] create ... create missing targets
47
- kibo [options] deploy ... updates all remote instances
48
- kibo [options] spinup ... starts all remote instances
49
- kibo [options] spindown ... stops all remote instances
50
- kibo [options] reconfigure ... reconfigure all existing targets
36
+ #{usage}
51
37
 
52
38
  where [options] are:
53
39
 
@@ -56,37 +42,42 @@ EOS
56
42
  opt :environment, "Set environment", :short => 'e', :type => String, :default => "staging"
57
43
  opt :kibofile, "Set Kibofile name", :short => 'k', :type => String, :default => "Kibofile"
58
44
  opt :procfile, "Set Procfile name", :short => 'p', :type => String, :default => "Procfile"
59
- opt :dry, "Do nothing", :short => 'n'
60
45
 
61
- stop_on SUBCOMMANDS
46
+ stop_on Kibo::Commands.commands
62
47
  end
63
48
 
64
49
  @subcommand = ARGV.shift # get the subcommand
65
50
 
66
- unless SUBCOMMANDS.include?(@subcommand)
67
- Trollop::die(@subcommand ? "Unknown subcommand #{@subcommand.inspect}" : "Missing subcommand")
51
+ unless Kibo::Commands.commands.include?(@subcommand)
52
+ if @subcommand
53
+ Trollop.die "Unknown subcommand #{@subcommand.inspect}"
54
+ else
55
+ Trollop.die "Missing subcommand"
56
+ end
68
57
  end
69
58
 
70
59
  # Is there a specific subcommand options configuration?
71
-
72
- subcommand_options =
73
- case @subcommand
74
- when "spinup"
75
- Trollop::options do
76
- opt :force, "Ignore missing targets.", :short => "f"
77
- end
78
- when "deploy"
79
- Trollop::options do
80
- opt :force, "Ignore outstanding changes.", :short => "f"
81
- end
82
- when "create"
83
- Trollop::options do
84
- opt :all, "Create all missing targets.", :short => "a"
85
- end
60
+
61
+ if proc = Kibo::Commands.options[@subcommand]
62
+ subcommand_options = Trollop::options do
63
+ instance_eval &proc
86
64
  end
87
-
88
- @options.update subcommand_options if subcommand_options
65
+
66
+ @options.update subcommand_options
67
+ end
89
68
 
90
69
  @args = ARGV.dup
91
70
  end
71
+
72
+ def method_missing(sym, *args, &block)
73
+ if block_given? || !args.empty?
74
+ super
75
+ elsif options.key?(sym)
76
+ options[sym]
77
+ elsif (sym.to_s =~ /(.*)\?/) && options.key?($1.to_sym)
78
+ !! options[$1.to_sym]
79
+ else
80
+ super
81
+ end
82
+ end
92
83
  end
@@ -0,0 +1,85 @@
1
+ module Kibo::Commands
2
+ subcommand :compress, "compress JS and CSS files" do
3
+ opt :quiet, "Be less verbose.", :short => "q"
4
+ end
5
+
6
+ def compress
7
+ dirs = Kibo.command_line.args
8
+ dirs.each do |dir| compress_dir(dir) end
9
+ end
10
+
11
+ private
12
+
13
+ def kibo_bin_path
14
+ File.join(File.dirname(__FILE__), "..", "bin")
15
+ end
16
+
17
+ def yuicompressor(*args)
18
+ yuicompressor = "#{kibo_bin_path}/yuicompressor-2.4.7.jar"
19
+ Kibo::System.sys! "java", "-jar", yuicompressor, *args
20
+ end
21
+
22
+ def compress_dir(dir)
23
+ css_files = Dir.glob File.join(dir, "**/*.css")
24
+ js_files = Dir.glob File.join(dir, "**/*.js")
25
+
26
+ files = css_files + js_files
27
+
28
+ old_sizes = files.inject({}) do |hash, file|
29
+ hash.update file => File.size(file)
30
+
31
+ file = file + ".gz"
32
+ hash.update file => File.size(file)
33
+ end
34
+
35
+ unless css_files.empty?
36
+ B "Compressing #{css_files.length} CSS files" do
37
+ yuicompressor "--type", "css", "-o", '.css$:.css.min', *css_files, :quiet
38
+ end
39
+ end
40
+ unless js_files.empty?
41
+ B "Compressing #{js_files.length} JS files" do
42
+ yuicompressor "--type", "js", "-o", '.js$:.js.min', *js_files, :quiet
43
+ end
44
+ end
45
+
46
+ unless files.empty?
47
+ B "gzipping #{files.length} files" do
48
+ files.each do |file|
49
+ FileUtils.cp "#{file}.min", file
50
+ Kibo::System.sys! "gzip", "-9", "-f", file, :quiet
51
+ FileUtils.mv "#{file}.min", file
52
+ end
53
+ end
54
+ end
55
+
56
+ Kibo::Helpers::Info.print do |info|
57
+ old_sum, new_sum = 0, 0
58
+
59
+ log_file = lambda do |file|
60
+ old_size, new_size = old_sizes[file], File.size(file)
61
+
62
+ unless Kibo.command_line.quiet?
63
+ info.line file, "#{old_size} -> #{new_size}"
64
+ end
65
+ old_sum += old_size
66
+ new_sum += new_size
67
+ end
68
+
69
+ info.head "JS" unless Kibo.command_line.quiet?
70
+
71
+ js_files.sort.each do |file|
72
+ log_file.call(file)
73
+ end
74
+
75
+ info.head "CSS" unless Kibo.command_line.quiet?
76
+
77
+ css_files.sort.each do |file|
78
+ log_file.call(file)
79
+ end
80
+
81
+ info.head "Summary" unless Kibo.command_line.quiet?
82
+ info.line "#{files.length} files", "#{old_sum} -> #{new_sum} byte: #{(new_sum * 100.0 / old_sum).round(1)} %"
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,56 @@
1
+ module Kibo::Commands
2
+ subcommand :create, "create missing targets" do
3
+ opt :all, "Create all missing targets.", :short => "a"
4
+ end
5
+
6
+ def create
7
+ verify_heroku_login
8
+
9
+ if Kibo.command_line.all?
10
+ instances = missing_remotes
11
+ if instances.empty?
12
+ W "Nothing to do."
13
+ exit 0
14
+ end
15
+ else
16
+ instances = Kibo.command_line.args
17
+ if instances.empty?
18
+ W "Add the names of the remotes to create on the command line or use the --all parameter."
19
+ exit 0
20
+ end
21
+
22
+ # only create instances that are actually missing.
23
+ extra_instances = instances - missing_remotes
24
+ unless extra_instances.empty?
25
+ E <<-MSG
26
+ kibo cannot create these instances for you: #{extra_instances.map(&:inspect).join(", ")}, because I don't not know anything about these.
27
+ MSG
28
+ end
29
+ end
30
+
31
+ confirm! <<-MSG
32
+ I am going to create these instances: #{instances.map(&:inspect).join(", ")}. Is this what you want? Note:
33
+ You are logged in at heroku as #{config.account}.
34
+ MSG
35
+
36
+ instances.each do |instance|
37
+ create_instance(instance)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def create_instance(remote)
44
+ # TODO: Test whether these instances already exist, using `heroku apps`
45
+ heroku "apps:create", remote, "--remote", remote
46
+ end
47
+
48
+ def verify_heroku_login
49
+ whoami = h.whoami
50
+ if !whoami
51
+ E "Please log in ('heroku auth:login') as #{config.account}."
52
+ elsif whoami != Kibo.config.account
53
+ E "You are currently logged in as #{whoami}; please log in ('heroku auth:login') as #{config.account}."
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,69 @@
1
+ module Kibo::Commands
2
+ subcommand :deploy, "updates all remote instances" do
3
+ opt :force, "Ignore outstanding changes.", :short => "f"
4
+ end
5
+
6
+ def deploy
7
+ if Kibo.command_line.force?
8
+ h.check_missing_remotes(:warn)
9
+ else
10
+ h.check_missing_remotes(:error)
11
+ end
12
+ #
13
+ # create a deployment branch, if there is none yet.
14
+ checkout_branch Kibo.environment
15
+
16
+ git "merge", "master"
17
+ run_commands Kibo.config.deployment["pre"]
18
+ W "pre commands done"
19
+
20
+ h.configured_remotes.each do |remote|
21
+ deploy_remote! remote
22
+ end
23
+
24
+ W "Deployment succeeded."
25
+ run_commands Kibo.config.deployment["post"]
26
+ rescue StandardError
27
+ W $!
28
+ raise
29
+ ensure
30
+ unless current_branch == "master"
31
+ git "reset", "--hard"
32
+ git "checkout", "master"
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def checkout_branch(name)
39
+ unless branches.include?(name)
40
+ Kibo::System.git "branch", name
41
+ end
42
+
43
+ git "checkout", name
44
+ end
45
+
46
+ def current_branch
47
+ `git branch`.split(/\n/).detect do |line|
48
+ line =~ /^* /
49
+ end.sub(/^\* /, "")
50
+ end
51
+
52
+ def branches
53
+ branches = `git branch`
54
+ ("\n" + branches).split(/\n[\* ]+/).reject(&:empty?)
55
+ end
56
+
57
+ def deploy_remote!(remote)
58
+ git "push", remote, "master"
59
+ end
60
+
61
+ def run_commands(commands)
62
+ return unless commands
63
+ commands = [ commands ] if commands.is_a?(String)
64
+
65
+ commands.each do |command|
66
+ Kibo::System.sh! command
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,53 @@
1
+ module Kibo::Commands
2
+ subcommand :generate, "generate an example Kibofile"
3
+
4
+ def generate
5
+ if File.exists?(Kibo.kibofile)
6
+ E "#{Kibo.kibofile}: already existing."
7
+ return
8
+ end
9
+
10
+ File.open(Kibo.kibofile, "w") do |io|
11
+ io.write kibofile_example
12
+ end
13
+ S "#{Kibo.kibofile}: created."
14
+ end
15
+
16
+ private
17
+
18
+ def kibofile_example
19
+ namespace = File.basename Dir.getwd
20
+ account = h.whoami || "user@domain.com"
21
+
22
+ kibo = <<-EXAMPLE
23
+ # This is an example Kibofile. Use with kibo(1) to configure
24
+ # remote instances.
25
+ heroku:
26
+ #
27
+ # The heroku account to create application instances on heroku.
28
+ account: #{account}
29
+
30
+ # You instances will be called 'kiboex-staging-web0', 'kiboex-production-worker0', etc.
31
+ namespace: kiboex
32
+
33
+ # What to do before and after deployment? These steps are run in the order
34
+ # defined here, and in an checked out deployment repository.
35
+ deployment:
36
+ pre:
37
+ - git rm -rf public/assets || true
38
+ - rake assets:rebuild
39
+ - kibo compress --quiet public/assets
40
+ - git add -f public/assets
41
+ - git commit -m '[kibo] Updated assets'
42
+ post:
43
+ - heroku run rake db:migrate
44
+ defaults:
45
+ web: 1
46
+ worker: 1
47
+ production:
48
+ web: 1
49
+ worker: 2
50
+ EXAMPLE
51
+ end
52
+ end
53
+
@@ -0,0 +1,19 @@
1
+ module Kibo::Commands
2
+ subcommand :info, "show information about the current settings"
3
+
4
+ def info
5
+ Kibo::Helpers::Info.print do |info|
6
+ info.head "general"
7
+ info.line "environment", Kibo.environment
8
+
9
+ info.head "heroku"
10
+ info.line "current account", h.whoami
11
+ info.line "expected account", Kibo.config.account
12
+
13
+ info.head "remotes"
14
+ info.line "remotes", h.expected_remotes
15
+ info.line "configured", h.configured_remotes
16
+ info.line "missing", h.missing_remotes
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ module Kibo::Commands
2
+ subcommand :reconfigure, "reconfigure all existing targets"
3
+
4
+ # kibo [options] reconfigure ... reconfigure all existing remotes
5
+ def reconfigure
6
+ check_missing_remotes :warn
7
+
8
+ configured_remotes.each do |remote|
9
+ configure_remote! remote
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,39 @@
1
+ # --spin up/spin down remotes
2
+ module Kibo::Commands
3
+ subcommand :spinup, "starts all remote instances" do
4
+ opt :force, "Ignore missing targets.", :short => "f"
5
+ end
6
+
7
+ subcommand :spindown, "stops all remote instances"
8
+
9
+ def spinup
10
+ check_missing_remotes(Kibo.command_line.force? ? :warn : :error)
11
+ spin Kibo.config.processes
12
+ end
13
+
14
+ def spindown
15
+ spin({})
16
+ end
17
+
18
+ private
19
+
20
+ def spin(processes)
21
+ Kibo.config.remotes_by_process.each do |name, remotes|
22
+ number_of_processes = processes[name] || 0
23
+
24
+ remotes.each do |remote|
25
+ if number_of_processes > 0
26
+ configure_remote remote
27
+ heroku "ps:scale", "#{name}=1", "--app", remote
28
+ number_of_processes -= 1
29
+ else
30
+ heroku "ps:scale", "#{name}=0", "--app", remote
31
+ end
32
+ end
33
+
34
+ if number_of_processes > 0
35
+ W "Missing #{name} remote(s)", number_of_processes
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,44 @@
1
+ require_relative "helpers"
2
+ require_relative "system"
3
+
4
+ module Kibo::Commands
5
+ extend self
6
+
7
+ def self.options
8
+ @options ||= {}
9
+ end
10
+
11
+ def self.descriptions
12
+ @descriptions ||= {}
13
+ end
14
+
15
+ def self.subcommand(name, description = nil, &block)
16
+ options[name.to_s] = Proc.new if block_given?
17
+ descriptions[name.to_s] = description
18
+ end
19
+
20
+ def self.commands
21
+ public_instance_methods.map(&:to_s)
22
+ end
23
+
24
+ private
25
+
26
+ def h
27
+ Kibo::Helpers
28
+ end
29
+
30
+ def sys
31
+ Kibo::System
32
+ end
33
+
34
+ def git(*args)
35
+ sys.git *args
36
+ end
37
+
38
+ def heroku(*args)
39
+ sys.heroku *args
40
+ end
41
+ end
42
+
43
+ subfiles = Dir.glob( __FILE__.gsub(/\.rb$/, "/*.rb")).sort
44
+ subfiles.each { |file| load file }
data/lib/kibo/config.rb CHANGED
@@ -17,6 +17,8 @@ class Kibo::Configfile < Hash
17
17
 
18
18
  update key => value
19
19
  end
20
+ rescue
21
+ E "No such file", path
20
22
  end
21
23
 
22
24
  def die(lineno, msg)
@@ -42,21 +44,18 @@ class Kibo::Config
42
44
  def initialize(path)
43
45
  super()
44
46
 
45
- @data = Hash.new do |hash, key|
46
- W "#{key}: missing setting for #{environment.inspect} environment, using default."
47
- hash[key] = DEFAULTS[key]
48
- end
49
-
47
+ @data = DEFAULTS.dup
48
+
50
49
  begin
51
50
  kibo = YAML.load File.read(path)
51
+ @data.update(kibo)
52
52
  @data.update(kibo["defaults"] || {})
53
53
  @data.update(kibo[environment] || {})
54
54
  rescue Errno::ENOENT
55
55
  W "No such file", path
56
- @data = DEFAULTS
57
56
  end
58
57
 
59
- @procfile = Kibo::Configfile.new(self["procfile"] || "Procfile")
58
+ @procfile = Kibo::Configfile.new(self["procfile"])
60
59
  end
61
60
 
62
61
  # processes are defined in the Procfile. The scaling, however, is defined in
@@ -70,12 +69,25 @@ class Kibo::Config
70
69
  #
71
70
  # we need namespace-ENVIRONMENT-process<1>
72
71
 
72
+ # returns the heroku configuration
73
+ def heroku
74
+ self["heroku"] || {}
75
+ end
76
+
77
+ # returns deployment specific configuration
78
+ def deployment
79
+ self["deployment"] || {}
80
+ end
81
+
82
+ # returns the heroku namespace
73
83
  def namespace
74
- self["namespace"] || raise("Please define a namespace in your Kibofile.")
84
+ heroku["namespace"] || E("Please set the heroku namespace in your Kibofile.")
75
85
  end
76
86
 
77
- def heroku
78
- self["heroku"] || raise("Please defined the heroku entry in your Kibofile")
87
+ # returns the heroku account email. This is the account that
88
+ # you should be logged in
89
+ def account
90
+ heroku["account"] || E("Please set the heroku account email in your Kibofile")
79
91
  end
80
92
 
81
93
  def remotes_by_process