kibo 0.1.2 → 0.3.0

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