kibo 0.1.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,75 @@
1
+ # It would be great if we could just use Heroku.read_credentials or `heroku whoami`
2
+ # to check for a user's current login. Well, we can't:
3
+ #
4
+ # - since heroku aggressively pushes its heroku toolbelt people
5
+ # might be forced to uninstall the heroku gem.
6
+ # - `heroku whoami` blocks for input, if the user is not logged in.
7
+ #
8
+ # Hence this code, which is based on code from the heroku gem.
9
+ #
10
+ # == Copyright and licensing ==================================================
11
+ #
12
+ # The heroku gem is licensed under the terms of the MIT license,
13
+ # see the file License.MIT.
14
+ #
15
+ # The heroku has been created by Adam Wiggins, and is currently maintained
16
+ # by Wesley Beary.
17
+ module Kibo::Helpers::Heroku
18
+ require "netrc"
19
+
20
+ def whoami
21
+ (read_credentials || []).first
22
+ end
23
+
24
+ private
25
+
26
+ def host
27
+ ENV['HEROKU_HOST'] || default_host
28
+ end
29
+
30
+ def default_host
31
+ "heroku.com"
32
+ end
33
+
34
+ def netrc_path
35
+ default = Netrc.default_path
36
+ encrypted = default + ".gpg"
37
+ if File.exists?(encrypted)
38
+ encrypted
39
+ else
40
+ default
41
+ end
42
+ end
43
+
44
+ def netrc # :nodoc:
45
+ @netrc ||= begin
46
+ File.exists?(netrc_path) && Netrc.read(netrc_path)
47
+ rescue => error
48
+ if error.message =~ /^Permission bits for/
49
+ perm = File.stat(netrc_path).mode & 0777
50
+ abort("Permissions #{perm} for '#{netrc_path}' are too open. You should run `chmod 0600 #{netrc_path}` so that your credentials are NOT accessible by others.")
51
+ else
52
+ raise error
53
+ end
54
+ end
55
+ end
56
+
57
+ def read_credentials
58
+ if ENV['HEROKU_API_KEY']
59
+ ['', ENV['HEROKU_API_KEY']]
60
+ else
61
+ # read netrc credentials if they exist
62
+ if netrc
63
+ # force migration of long api tokens (80 chars) to short ones (40)
64
+ # #write_credentials rewrites both api.* and code.*
65
+ credentials = netrc["api.#{host}"]
66
+ if credentials && credentials[1].length > 40
67
+ @credentials = [ credentials[0], credentials[1][0,40] ]
68
+ write_credentials
69
+ end
70
+
71
+ netrc["api.#{host}"]
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,50 @@
1
+ class Kibo::Helpers::Info < Array
2
+ def self.print(out = STDOUT, &block)
3
+ out.puts build(&block).to_s
4
+ end
5
+
6
+ def self.build(&block)
7
+ info = new
8
+ yield info
9
+ info
10
+ end
11
+
12
+ def head(msg)
13
+ push [ :head, msg ]
14
+ end
15
+
16
+ def line(msg, value)
17
+ push [ :line, msg, value ]
18
+ end
19
+
20
+ def to_s
21
+ key_length = map do |kind, msg, value|
22
+ kind == :line ? msg.length : 0
23
+ end.max
24
+
25
+ key_length = 60 if key_length > 60
26
+
27
+ key_format = "%#{key_length + 4}s"
28
+
29
+ map do |kind, msg, value|
30
+ case kind
31
+ when :head
32
+ "== #{msg} " + "=" * (100 - msg.length)
33
+ when :line
34
+ case value
35
+ when [], nil then value = "<none>"
36
+ when Array then value = value.map(&:inspect).join(", ")
37
+ end
38
+
39
+ if msg == ""
40
+ "#{key_format % msg} #{value}"
41
+ elsif msg.length > key_length
42
+ msg = msg[0..20] + "..." + msg[(25 - key_length) ..-1]
43
+ "#{key_format % msg}: #{value}"
44
+ else
45
+ "#{key_format % msg}: #{value}"
46
+ end
47
+ end
48
+ end.join("\n")
49
+ end
50
+ end
@@ -0,0 +1,70 @@
1
+ module Kibo::Helpers
2
+ end
3
+
4
+ require_relative "./helpers/heroku"
5
+ require_relative "./helpers/info"
6
+
7
+ module Kibo::Helpers
8
+ extend self
9
+
10
+ extend Heroku
11
+
12
+ def check_missing_remotes(mode = :warn)
13
+ return if missing_remotes.empty?
14
+
15
+ if mode == :warn
16
+ W "Ignoring missing remote(s)", *missing_remotes
17
+ return
18
+ end
19
+
20
+ E <<-MSG
21
+ Missing remote(s): #{missing_remotes.map(&:inspect).join(", ")}. Run
22
+
23
+ kibo --environment #{environment} create --all # ... to create all missing remotes.
24
+ kibo --environment #{environment} spinup --force # ... to ignore missing remotes.
25
+
26
+ MSG
27
+ end
28
+
29
+ # -- configure remotes ----------------------------------------------
30
+
31
+ def configure_remote!(remote)
32
+ heroku "config:set",
33
+ "RACK_ENV=#{environment}",
34
+ "RAILS_ENV=#{environment}",
35
+ "INSTANCE=#{instance_for_remote(remote)}",
36
+ "--app", remote
37
+ end
38
+
39
+ def instance_for_remote(remote)
40
+ remote[config.namespace.length + 1 .. -1]
41
+ end
42
+
43
+ def configure_remote(remote)
44
+ # the correct value for the INSTANCE configuration setting is the
45
+ # name of the of the remote without the namespace part; e.g. the
46
+ # INSTANCE for the remote named "bountyhill-staging-twirl2" is
47
+ # "staging-twirl2".
48
+ instance = remote[config.namespace.length + 1 .. -1]
49
+ current_instance = heroku "config:get", "INSTANCE", "--app", remote
50
+ return if instance == current_instance
51
+ end
52
+
53
+ # -- which remotes are defined, present and configured --------------
54
+
55
+ def expected_remotes
56
+ namespace, environment = Kibo.config.namespace, Kibo.environment
57
+
58
+ Kibo.config.processes.map do |name, count|
59
+ 1.upto(count).map { |idx| "#{namespace}-#{environment}-#{name}#{idx}" }
60
+ end.flatten.sort
61
+ end
62
+
63
+ def configured_remotes
64
+ Kibo.config.remotes_by_process.values.flatten
65
+ end
66
+
67
+ def missing_remotes
68
+ expected_remotes - configured_remotes
69
+ end
70
+ end
data/lib/kibo/log.rb CHANGED
@@ -1,18 +1,44 @@
1
+ require "bundler/ui"
2
+ require "thor/shell"
3
+
4
+ UI = Bundler::UI::Shell.new(Thor::Shell::Color.new)
5
+
1
6
  def log_message(msg, *args)
2
7
  return msg if args.empty?
3
8
  "#{msg}: " + args.map(&:inspect).join(", ")
4
9
  end
5
10
 
6
11
  def D(*args)
7
- STDERR.puts log_message(*args)
12
+ UI.info log_message(*args)
8
13
  end
9
14
 
10
15
  def W(*args)
11
- STDERR.puts log_message(*args)
16
+ UI.warn log_message(*args)
12
17
  end
13
18
 
14
19
  def E(*args)
15
- STDERR.puts log_message(*args)
20
+ UI.error log_message(*args)
16
21
  exit 1
17
22
  end
18
23
 
24
+ def B(*args, &block)
25
+ msg = log_message(*args)
26
+ W msg
27
+
28
+ start = Time.now
29
+ yield.tap do
30
+ W "#{msg}: #{(1000 * (Time.now - start)).to_i} msecs."
31
+ end
32
+ end
33
+
34
+ # Success!
35
+ def S(*args)
36
+ UI.confirm log_message(*args)
37
+ end
38
+
39
+ def confirm!(msg)
40
+ puts msg
41
+ puts "\n\nPress ^C to abort or return to continue."
42
+
43
+ STDIN.readline
44
+ end
data/lib/kibo/system.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Kibo::System
2
2
  extend self
3
-
3
+
4
4
  def heroku(*args)
5
5
  sys! "heroku", *args
6
6
  end
@@ -10,22 +10,42 @@ module Kibo::System
10
10
  end
11
11
 
12
12
  def sys(*args)
13
+ cmd = build_command(*args)
14
+ result = Kernel.send "`", "bash -c \"#{cmd}\""
15
+ if command_succeeded?(cmd)
16
+ result.chomp
17
+ end
18
+ end
19
+
20
+ def sys!(*args)
21
+ sys(*args) || exit(1)
22
+ end
23
+
24
+ def sh(*args)
25
+ cmd = build_command(*args)
26
+ system(cmd)
27
+ command_succeeded?(cmd)
28
+ end
29
+
30
+ def sh!(*args)
31
+ sh(*args) || exit(1)
32
+ end
33
+
34
+ private
35
+
36
+ def build_command(*args)
13
37
  quiet = args.pop if args.last == :quiet
38
+ args[0].sub!(/^kibo\b/, $0)
14
39
  cmd = args.map(&:to_s).join(" ")
15
40
  W cmd unless quiet
16
-
17
- # A command is run because it either is "quiet", i.e. is non-obstrusive anyway,
18
- # or we are not in a dry. Dry run mode could go with some improvements, though.
19
- if quiet || !Kibo::CommandLine.dry?
20
- stdout = Kernel.send "`", "bash -c \"#{cmd}\""
21
- stdout.chomp if $?.exitstatus == 0
22
- else
23
- ""
24
- end
41
+ cmd
25
42
  end
26
43
 
27
- def sys!(*args)
28
- sys(*args) || E("Command failed: " + args.join(" "))
44
+ def command_succeeded?(cmd)
45
+ return true if $?.exitstatus == 0
46
+
47
+ UI.error("Command failed: #{cmd}")
48
+ false
29
49
  end
30
50
  end
31
51
 
data/lib/kibo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kibo
2
- VERSION = "0.1.2"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/kibo.rb CHANGED
@@ -7,215 +7,29 @@ end
7
7
  require_relative "kibo/log"
8
8
  require_relative "kibo/system"
9
9
  require_relative "kibo/config"
10
+ require_relative "kibo/commands"
10
11
  require_relative "kibo/commandline"
11
12
 
12
13
  module Kibo
13
14
  extend self
14
-
15
+
15
16
  def config
16
- @config ||= Config.new(CommandLine.kibofile)
17
+ @config ||= Config.new(kibofile)
17
18
  end
18
19
 
19
20
  def environment
20
- Kibo::CommandLine.environment
21
- end
22
-
23
- def expected_remotes
24
- config.processes.inject([]) do |ary, (name, count)|
25
- ary.concat 1.upto(count).map { |idx| "#{config.namespace}-#{environment}-#{name}#{idx}" }
26
- end
27
- end
28
-
29
- def configured_remotes
30
- config.remotes_by_process.values.flatten
31
- end
32
-
33
- def missing_remotes
34
- expected_remotes - configured_remotes
35
- end
36
-
37
- # -- configure a remote
38
-
39
- private
40
-
41
- def instance_for_remote(remote)
42
- remote[config.namespace.length + 1 .. -1]
43
- end
44
-
45
- public
46
-
47
- def configure_remote!(remote)
48
- heroku "config:set",
49
- "RACK_ENV=#{environment}",
50
- "RAILS_ENV=#{environment}",
51
- "INSTANCE=#{instance_for_remote(remote)}",
52
- "--app", remote
53
- end
54
-
55
- def configure_remote(remote)
56
- # the correct value for the INSTANCE configuration setting is the
57
- # name of the of the remote without the namespace part; e.g. the
58
- # INSTANCE for the remote named "bountyhill-staging-twirl2" is
59
- # "staging-twirl2".
60
- instance = remote[config.namespace.length + 1 .. -1]
61
- current_instance = heroku "config:get", "INSTANCE", "--app", remote
62
- return if instance == current_instance
63
- end
64
-
65
- # --spin up/spin down remotes
66
-
67
- private
68
-
69
- def spin(processes)
70
- config.remotes_by_process.each do |name, remotes|
71
- number_of_processes = processes[name] || 0
72
-
73
- remotes.each do |remote|
74
- if number_of_processes > 0
75
- configure_remote remote
76
- heroku "ps:scale", "#{name}=1", "--app", remote
77
- number_of_processes -= 1
78
- else
79
- heroku "ps:scale", "#{name}=0", "--app", remote
80
- end
81
- end
82
-
83
- if number_of_processes > 0
84
- W "Missing #{name} remote(s)", number_of_processes
85
- end
86
- end
21
+ CommandLine.environment
87
22
  end
88
23
 
89
- public
90
-
91
- def spinup
92
- check_missing_remotes(CommandLine.force? ? :warn : :error)
93
- spin config.processes
94
- end
95
-
96
- def spindown
97
- spin({})
98
- end
99
-
100
- # reconfigure existing remotes
101
- public
102
-
103
- # kibo [options] reconfigure ... reconfigure all existing remotes
104
- def reconfigure
105
- check_missing_remotes :warn
106
-
107
- configured_remotes.each do |remote|
108
- configure_remote! remote
109
- end
110
- end
111
-
112
- private
113
-
114
- def prepare_deployment
115
- end
116
-
117
- def deploy_remote!(remote)
118
- git "push", remote, "master"
119
- end
120
-
121
- public
122
-
123
- def deploy
124
- check_missing_remotes(CommandLine.force? ? :warn : :error)
125
-
126
- prepare_deployment
127
- configured_remotes.each do |remote|
128
- deploy_remote! remote
129
- end
130
-
131
- W "Deployment succeeded."
24
+ def run
25
+ Commands.send CommandLine.subcommand
132
26
  end
133
-
134
- private
135
-
136
- def check_missing_remotes(mode = :warn)
137
- return if missing_remotes.empty?
138
-
139
- if mode == :warn
140
- W "Ignoring missing remote(s)", *missing_remotes
141
- return
142
- end
143
-
144
- E <<-MSG
145
- Missing remote(s): #{missing_remotes.map(&:inspect).join(", ")}. Run
146
27
 
147
- kibo --environment #{environment} create --all # ... to create all missing remotes.
148
- kibo --environment #{environment} spinup --force # ... to ignore missing remotes.
149
-
150
- MSG
28
+ def command_line
29
+ CommandLine
151
30
  end
152
-
153
- public
154
-
155
- def create
156
- verify_heroku_login
157
-
158
- if CommandLine.all?
159
- instances = missing_remotes
160
- if instances.empty?
161
- W "Nothing to do."
162
- exit 0
163
- end
164
- else
165
- instances = CommandLine.args
166
- if instances.empty?
167
- W "Add the names of the remotes to create on the command line or use the --all parameter."
168
- exit 0
169
- end
170
31
 
171
- # only create instances that are actually missing.
172
- extra_instances = instances - missing_remotes
173
- unless extra_instances.empty?
174
- E <<-MSG
175
- kibo cannot create these instances for you: #{extra_instances.map(&:inspect).join(", ")}, because I don't not know anything about these.
176
- MSG
177
- end
178
- end
179
-
180
- confirm! <<-MSG
181
- I am going to create these instances: #{instances.map(&:inspect).join(", ")}. Is this what you want? Note:
182
- You are logged in at heroku as #{config.heroku}.
183
- MSG
184
-
185
- instances.each do |instance|
186
- create_instance(instance)
187
- end
188
- end
189
-
190
- def confirm!(msg)
191
- puts msg
192
- puts "\n\nPress ^C to abort or return to continue."
193
-
194
- STDIN.read
195
- end
196
-
197
- private
198
-
199
- def verify_heroku_login
200
- (Heroku::Auth.read_credentials || []).first.tap do |whoami|
201
- msg = if !whoami
202
- "Please log into heroku as #{config.heroku.inspect}."
203
- elsif whoami != config.heroku
204
- "You are currently logged into heroku as #{whoami.inspect}; please log in as #{config.heroku.inspect}."
205
- end
206
-
207
- E "#{msg}\n\n\theroku auth:login" if msg
208
- end
209
- end
210
-
211
- def create_instance(remote)
212
- # TODO: Test whether these instances already exist, using `heroku apps`
213
- heroku "apps:create", remote, "--remote", remote
214
- end
215
-
216
- public
217
-
218
- def run
219
- self.send CommandLine.subcommand
32
+ def kibofile
33
+ command_line.kibofile
220
34
  end
221
35
  end
data/man/kibo.1 CHANGED
@@ -1,7 +1,7 @@
1
1
  .\" generated with Ronn/v0.7.3
2
2
  .\" http://github.com/rtomayko/ronn/tree/0.7.3
3
3
  .
4
- .TH "KIBO" "1" "August 2012" "Kibo 0.1.2" "Kibo Manual"
4
+ .TH "KIBO" "1" "September 2012" "Kibo 0.3.0" "Kibo Manual"
5
5
  .
6
6
  .SH "NAME"
7
7
  \fBkibo\fR \- manage heroku applications
@@ -21,41 +21,94 @@
21
21
  .br
22
22
  \fBkibo [options] reconfigure\fR
23
23
  .
24
+ .br
25
+ \fBkibo [options] generate\fR
26
+ .
24
27
  .SH "DESCRIPTION"
25
- kibo is an awesome program that does something very, very important\.
28
+ kibo manages multiple application roles on single heroku dynos\.
26
29
  .
27
30
  .SH "DESCRIPTION"
28
- Foreman is a manager for Procfile\-based applications\. Its aim is to abstract away the details of the Procfile format, and allow you to either run your application directly or export it to some other process management format\.
31
+ Kibo is a perfect addition to Procfile based deployment on heroku\.com\. While heroku itself provides adequate tools to manage a single application on multiple dynos, Kibo adds tools to manage multiple application roles on single dynos with automatic instance provisioning\.
32
+ .
33
+ .P
34
+ The application roles are read from the Procfile (see foreman(1))\. The concurrency options \- i\.e\. the number of applications to run each role \- is read from the Kibofile\.
35
+ .
36
+ .SH "INSTANCE PROVISIONING"
37
+ Each instance gets automatically configured using `heroku config\. kibo sets the environment variables INSTANCE, RAILS_ENV and RACK_ENV to reflect instance role and number and runtime environment\.
38
+ .
39
+ .IP "" 4
40
+ .
41
+ .nf
42
+
43
+ INSTANCE="kibo\-staging\-web1"
44
+ RAILS_ENV="staging"
45
+ RACK_ENV="staging"
46
+ .
47
+ .fi
48
+ .
49
+ .IP "" 0
29
50
  .
30
51
  .SH "GLOBAL OPTIONS"
31
52
  The following options control how kibo is run:
32
53
  .
33
54
  .TP
34
55
  \fB\-e\fR, \fB\-\-environment\fR
35
- Set the target environment\.
56
+ Set the target environment\. Defaults to "staging"
36
57
  .
37
58
  .TP
38
59
  \fB\-k\fR, \fB\-\-kibofile\fR
39
- Specify the Kibofile to use\.
60
+ Specify an alternate Kibofile to use\.
40
61
  .
41
62
  .TP
42
63
  \fB\-p\fR, \fB\-\-procfile\fR
43
- Specify an alternate Procfile to load\.
64
+ Specify an alternate Procfile to use\.
44
65
  .
45
66
  .SH "Kibofile"
46
- An example Kibofile is this:
67
+ A Kibofile scaffold can be generated via \fBkibo generate\fR\. The following is an example:
47
68
  .
48
69
  .IP "" 4
49
70
  .
50
71
  .nf
51
72
 
73
+ kibo:
74
+ # The email of the heroku account to create app instances on heroku\.
75
+ heroku: user@domain\.com
76
+ # You instances will be called \'kiboex\-staging\-web0\', \'kiboex\-production\-worker0\', etc\.
77
+ namespace: kibo
52
78
  defaults:
53
- procfile: Procfile
54
- namespace: radiospiel
55
- staging:
56
- twirl: 1
79
+ procfile: Procfile\.other
80
+ web: 1
81
+ worker: 1
57
82
  production:
58
- twirl: 2
83
+ web: 1
84
+ worker: 2
85
+ .
86
+ .fi
87
+ .
88
+ .IP "" 0
89
+ .
90
+ .P
91
+ This defines the roles "web" and "worker", which are running at one resp\. two instances in the "production" environment\.
92
+ .
93
+ .SH "Example session"
94
+ This is a session using the example Kibofile from above:
95
+ .
96
+ .IP "" 4
97
+ .
98
+ .nf
99
+
100
+ # creates the heroku applications "kibo\-production\-web1",
101
+ # "kibo\-production\-worker1", and "kibo\-production\-worker2"\.
102
+ kibo \-e production create
103
+
104
+ # deploy all applications\.
105
+ kibo \-e production deploy
106
+
107
+ # start all instances
108
+ kibo \-e production spinup
109
+
110
+ # stop all instances
111
+ kibo \-e production spindown
59
112
  .
60
113
  .fi
61
114
  .