azuki 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +71 -0
  3. data/bin/azuki +17 -0
  4. data/data/cacert.pem +3988 -0
  5. data/lib/azuki.rb +17 -0
  6. data/lib/azuki/auth.rb +339 -0
  7. data/lib/azuki/cli.rb +38 -0
  8. data/lib/azuki/client.rb +764 -0
  9. data/lib/azuki/client/azuki_postgresql.rb +141 -0
  10. data/lib/azuki/client/cisaurus.rb +26 -0
  11. data/lib/azuki/client/pgbackups.rb +113 -0
  12. data/lib/azuki/client/rendezvous.rb +108 -0
  13. data/lib/azuki/client/ssl_endpoint.rb +25 -0
  14. data/lib/azuki/command.rb +294 -0
  15. data/lib/azuki/command/account.rb +23 -0
  16. data/lib/azuki/command/accounts.rb +34 -0
  17. data/lib/azuki/command/addons.rb +305 -0
  18. data/lib/azuki/command/apps.rb +393 -0
  19. data/lib/azuki/command/auth.rb +86 -0
  20. data/lib/azuki/command/base.rb +230 -0
  21. data/lib/azuki/command/certs.rb +209 -0
  22. data/lib/azuki/command/config.rb +137 -0
  23. data/lib/azuki/command/db.rb +218 -0
  24. data/lib/azuki/command/domains.rb +85 -0
  25. data/lib/azuki/command/drains.rb +46 -0
  26. data/lib/azuki/command/fork.rb +164 -0
  27. data/lib/azuki/command/git.rb +64 -0
  28. data/lib/azuki/command/help.rb +179 -0
  29. data/lib/azuki/command/keys.rb +115 -0
  30. data/lib/azuki/command/labs.rb +147 -0
  31. data/lib/azuki/command/logs.rb +45 -0
  32. data/lib/azuki/command/maintenance.rb +61 -0
  33. data/lib/azuki/command/pg.rb +269 -0
  34. data/lib/azuki/command/pgbackups.rb +329 -0
  35. data/lib/azuki/command/plugins.rb +110 -0
  36. data/lib/azuki/command/ps.rb +232 -0
  37. data/lib/azuki/command/regions.rb +22 -0
  38. data/lib/azuki/command/releases.rb +124 -0
  39. data/lib/azuki/command/run.rb +180 -0
  40. data/lib/azuki/command/sharing.rb +89 -0
  41. data/lib/azuki/command/ssl.rb +43 -0
  42. data/lib/azuki/command/stack.rb +62 -0
  43. data/lib/azuki/command/status.rb +51 -0
  44. data/lib/azuki/command/update.rb +47 -0
  45. data/lib/azuki/command/version.rb +23 -0
  46. data/lib/azuki/deprecated.rb +5 -0
  47. data/lib/azuki/deprecated/help.rb +38 -0
  48. data/lib/azuki/distribution.rb +9 -0
  49. data/lib/azuki/excon.rb +9 -0
  50. data/lib/azuki/helpers.rb +517 -0
  51. data/lib/azuki/helpers/azuki_postgresql.rb +165 -0
  52. data/lib/azuki/helpers/log_displayer.rb +70 -0
  53. data/lib/azuki/plugin.rb +163 -0
  54. data/lib/azuki/updater.rb +171 -0
  55. data/lib/azuki/version.rb +3 -0
  56. data/lib/vendor/azuki/okjson.rb +598 -0
  57. data/spec/azuki/auth_spec.rb +256 -0
  58. data/spec/azuki/client/azuki_postgresql_spec.rb +71 -0
  59. data/spec/azuki/client/pgbackups_spec.rb +43 -0
  60. data/spec/azuki/client/rendezvous_spec.rb +62 -0
  61. data/spec/azuki/client/ssl_endpoint_spec.rb +48 -0
  62. data/spec/azuki/client_spec.rb +564 -0
  63. data/spec/azuki/command/addons_spec.rb +601 -0
  64. data/spec/azuki/command/apps_spec.rb +351 -0
  65. data/spec/azuki/command/auth_spec.rb +38 -0
  66. data/spec/azuki/command/base_spec.rb +109 -0
  67. data/spec/azuki/command/certs_spec.rb +178 -0
  68. data/spec/azuki/command/config_spec.rb +144 -0
  69. data/spec/azuki/command/db_spec.rb +110 -0
  70. data/spec/azuki/command/domains_spec.rb +87 -0
  71. data/spec/azuki/command/drains_spec.rb +34 -0
  72. data/spec/azuki/command/fork_spec.rb +56 -0
  73. data/spec/azuki/command/git_spec.rb +144 -0
  74. data/spec/azuki/command/help_spec.rb +93 -0
  75. data/spec/azuki/command/keys_spec.rb +120 -0
  76. data/spec/azuki/command/labs_spec.rb +100 -0
  77. data/spec/azuki/command/logs_spec.rb +60 -0
  78. data/spec/azuki/command/maintenance_spec.rb +51 -0
  79. data/spec/azuki/command/pg_spec.rb +236 -0
  80. data/spec/azuki/command/pgbackups_spec.rb +307 -0
  81. data/spec/azuki/command/plugins_spec.rb +104 -0
  82. data/spec/azuki/command/ps_spec.rb +195 -0
  83. data/spec/azuki/command/releases_spec.rb +130 -0
  84. data/spec/azuki/command/run_spec.rb +83 -0
  85. data/spec/azuki/command/sharing_spec.rb +59 -0
  86. data/spec/azuki/command/stack_spec.rb +46 -0
  87. data/spec/azuki/command/status_spec.rb +48 -0
  88. data/spec/azuki/command/version_spec.rb +16 -0
  89. data/spec/azuki/command_spec.rb +211 -0
  90. data/spec/azuki/helpers/azuki_postgresql_spec.rb +155 -0
  91. data/spec/azuki/helpers_spec.rb +48 -0
  92. data/spec/azuki/plugin_spec.rb +172 -0
  93. data/spec/azuki/updater_spec.rb +44 -0
  94. data/spec/helper/legacy_help.rb +16 -0
  95. data/spec/spec.opts +1 -0
  96. data/spec/spec_helper.rb +224 -0
  97. data/spec/support/display_message_matcher.rb +49 -0
  98. data/spec/support/openssl_mock_helper.rb +8 -0
  99. metadata +211 -0
@@ -0,0 +1,329 @@
1
+ require "azuki/client/pgbackups"
2
+ require "azuki/command/base"
3
+ require "azuki/helpers/azuki_postgresql"
4
+
5
+ module Azuki::Command
6
+
7
+ # manage backups of azuki postgresql databases
8
+ class Pgbackups < Base
9
+
10
+ include Azuki::Helpers::AzukiPostgresql
11
+
12
+ # pgbackups
13
+ #
14
+ # list captured backups
15
+ #
16
+ def index
17
+ validate_arguments!
18
+
19
+ backups = []
20
+ pgbackup_client.get_transfers.each { |t|
21
+ next unless backup_types.member?(t['to_name']) && !t['error_at'] && !t['destroyed_at']
22
+ backups << {
23
+ 'id' => backup_name(t['to_url']),
24
+ 'created_at' => t['created_at'],
25
+ 'status' => transfer_status(t),
26
+ 'size' => t['size'],
27
+ 'database' => t['from_name']
28
+ }
29
+ }
30
+
31
+ if backups.empty?
32
+ no_backups_error!
33
+ else
34
+ display_table(
35
+ backups,
36
+ %w{ id created_at status size database },
37
+ ["ID", "Backup Time", "Status", "Size", "Database"]
38
+ )
39
+ end
40
+ end
41
+
42
+ # pgbackups:url [BACKUP_ID]
43
+ #
44
+ # get a temporary URL for a backup
45
+ #
46
+ def url
47
+ name = shift_argument
48
+ validate_arguments!
49
+
50
+ if name
51
+ b = pgbackup_client.get_backup(name)
52
+ else
53
+ b = pgbackup_client.get_latest_backup
54
+ end
55
+ unless b['public_url']
56
+ error("No backup found.")
57
+ end
58
+ if $stdout.isatty
59
+ display '"'+b['public_url']+'"'
60
+ else
61
+ display b['public_url']
62
+ end
63
+ end
64
+
65
+ # pgbackups:capture [DATABASE]
66
+ #
67
+ # capture a backup from a database id
68
+ #
69
+ # if no DATABASE is specified, defaults to DATABASE_URL
70
+ #
71
+ # -e, --expire # if no slots are available, destroy the oldest manual backup to make room
72
+ #
73
+ def capture
74
+ attachment = hpg_resolve(shift_argument, "DATABASE_URL")
75
+ validate_arguments!
76
+
77
+ from_name = attachment.display_name
78
+ from_url = attachment.url
79
+ to_url = nil # server will assign
80
+ to_name = "BACKUP"
81
+
82
+ opts = {:expire => options[:expire]}
83
+
84
+ backup = transfer!(from_url, from_name, to_url, to_name, opts)
85
+
86
+ to_uri = URI.parse backup["to_url"]
87
+ backup_id = to_uri.path.empty? ? "error" : File.basename(to_uri.path, '.*')
88
+ display "\n#{from_name} ----backup---> #{backup_id}"
89
+
90
+ backup = poll_transfer!(backup)
91
+
92
+ if backup["error_at"]
93
+ message = "An error occurred and your backup did not finish."
94
+ message += "\nPlease run `azuki logs --ps pgbackups` for details."
95
+ message += "\nThe database is not yet online. Please try again." if backup['log'] =~ /Name or service not known/
96
+ message += "\nThe database credentials are incorrect." if backup['log'] =~ /psql: FATAL:/
97
+ error(message)
98
+ end
99
+ end
100
+
101
+ # pgbackups:restore [<DATABASE> [BACKUP_ID|BACKUP_URL]]
102
+ #
103
+ # restore a backup to a database
104
+ #
105
+ # if no DATABASE is specified, defaults to DATABASE_URL and latest backup
106
+ # if DATABASE is specified, but no BACKUP_ID, defaults to latest backup
107
+ #
108
+ def restore
109
+ if 0 == args.size
110
+ attachment = hpg_resolve(nil, "DATABASE_URL")
111
+ to_name = attachment.display_name
112
+ to_url = attachment.url
113
+ backup_id = :latest
114
+ elsif 1 == args.size
115
+ attachment = hpg_resolve(shift_argument)
116
+ to_name = attachment.display_name
117
+ to_url = attachment.url
118
+ backup_id = :latest
119
+ else
120
+ attachment = hpg_resolve(shift_argument)
121
+ to_name = attachment.display_name
122
+ to_url = attachment.url
123
+ backup_id = shift_argument
124
+ end
125
+
126
+ if :latest == backup_id
127
+ backup = pgbackup_client.get_latest_backup
128
+ no_backups_error! if {} == backup
129
+ to_uri = URI.parse backup["to_url"]
130
+ backup_id = File.basename(to_uri.path, '.*')
131
+ backup_id = "#{backup_id} (most recent)"
132
+ from_url = backup["to_url"]
133
+ from_name = "BACKUP"
134
+ elsif backup_id =~ /^http(s?):\/\//
135
+ from_url = backup_id
136
+ from_name = "EXTERNAL_BACKUP"
137
+ from_uri = URI.parse backup_id
138
+ backup_id = from_uri.path.empty? ? from_uri : File.basename(from_uri.path)
139
+ else
140
+ backup = pgbackup_client.get_backup(backup_id)
141
+ abort("Backup #{backup_id} already destroyed.") if backup["destroyed_at"]
142
+
143
+ from_url = backup["to_url"]
144
+ from_name = "BACKUP"
145
+ end
146
+
147
+ message = "#{to_name} <---restore--- "
148
+ padding = " " * message.length
149
+ display "\n#{message}#{backup_id}"
150
+ if backup
151
+ display padding + "#{backup['from_name']}"
152
+ display padding + "#{backup['created_at']}"
153
+ display padding + "#{backup['size']}"
154
+ end
155
+
156
+ if confirm_command
157
+ restore = transfer!(from_url, from_name, to_url, to_name)
158
+ restore = poll_transfer!(restore)
159
+
160
+ if restore["error_at"]
161
+ message = "An error occurred and your restore did not finish."
162
+ if restore['log'] =~ /Invalid dump format: .*: XML document text/
163
+ message += "\nThe backup url is invalid. Use `pgbackups:url` to generate a new temporary URL."
164
+ else
165
+ message += "\nPlease run `azuki logs --ps pgbackups` for details."
166
+ end
167
+ error(message)
168
+ end
169
+ end
170
+ end
171
+
172
+ # pgbackups:destroy BACKUP_ID
173
+ #
174
+ # destroys a backup
175
+ #
176
+ def destroy
177
+ unless name = shift_argument
178
+ error("Usage: azuki pgbackups:destroy BACKUP_ID\nMust specify BACKUP_ID to destroy.")
179
+ end
180
+ backup = pgbackup_client.get_backup(name)
181
+ if backup["destroyed_at"]
182
+ error("Backup #{name} already destroyed.")
183
+ end
184
+
185
+ action("Destroying #{name}") do
186
+ pgbackup_client.delete_backup(name)
187
+ end
188
+ end
189
+
190
+ protected
191
+
192
+ def transfer_status(t)
193
+ if t['finished_at']
194
+ "Finished @ #{t["finished_at"]}"
195
+ elsif t['started_at']
196
+ step = t['progress'] && t['progress'].split[0]
197
+ step.nil? ? 'Unknown' : step_map[step]
198
+ else
199
+ "Unknown"
200
+ end
201
+ end
202
+
203
+ def config_vars
204
+ @config_vars ||= api.get_config_vars(app).body
205
+ end
206
+
207
+ def pgbackup_client
208
+ pgbackups_url = ENV["PGBACKUPS_URL"] || config_vars["PGBACKUPS_URL"]
209
+ error("Please add the pgbackups addon first via:\nazuki addons:add pgbackups") unless pgbackups_url
210
+ @pgbackup_client ||= Azuki::Client::Pgbackups.new(pgbackups_url)
211
+ end
212
+
213
+ def backup_name(to_url)
214
+ # translate s3://bucket/email/foo/bar.dump => foo/bar
215
+ parts = to_url.split('/')
216
+ parts.slice(4..-1).join('/').gsub(/\.dump$/, '')
217
+ end
218
+
219
+ def transfer!(from_url, from_name, to_url, to_name, opts={})
220
+ pgbackup_client.create_transfer(from_url, from_name, to_url, to_name, opts)
221
+ end
222
+
223
+ def poll_error(app)
224
+ error <<-EOM
225
+ Failed to query the PGBackups status API. Your backup may still be running.
226
+ Verify the status of your backup with `azuki pgbackups -a #{app}`
227
+ You can also watch progress with `azuki logs --tail --ps pgbackups -a #{app}`
228
+ EOM
229
+ end
230
+
231
+ def poll_transfer!(transfer)
232
+ display "\n"
233
+
234
+ if transfer["errors"]
235
+ transfer["errors"].values.flatten.each { |e|
236
+ output_with_bang "#{e}"
237
+ }
238
+ abort
239
+ end
240
+
241
+ while true
242
+ update_display(transfer)
243
+ break if transfer["finished_at"]
244
+
245
+ sleep_time = 1
246
+ begin
247
+ sleep(sleep_time)
248
+ transfer = pgbackup_client.get_transfer(transfer["id"])
249
+ rescue
250
+ if sleep_time > 300
251
+ poll_error(app)
252
+ else
253
+ sleep_time *= 2
254
+ retry
255
+ end
256
+ end
257
+ end
258
+
259
+ display "\n"
260
+
261
+ return transfer
262
+ end
263
+
264
+ def step_map
265
+ @step_map ||= {
266
+ "dump" => "Capturing",
267
+ "upload" => "Storing",
268
+ "download" => "Retrieving",
269
+ "restore" => "Restoring",
270
+ "gunzip" => "Uncompressing",
271
+ "load" => "Restoring",
272
+ }
273
+ end
274
+
275
+ def update_display(transfer)
276
+ @ticks ||= 0
277
+ @last_updated_at ||= 0
278
+ @last_logs ||= []
279
+ @last_progress ||= ["", 0]
280
+
281
+ @ticks += 1
282
+
283
+ if !transfer["log"]
284
+ @last_progress = ['pending', nil]
285
+ redisplay "Pending... #{spinner(@ticks)}"
286
+ else
287
+ logs = transfer["log"].split("\n")
288
+ new_logs = logs - @last_logs
289
+ @last_logs = logs
290
+
291
+ new_logs.each do |line|
292
+ matches = line.scan /^([a-z_]+)_progress:\s+([^ ]+)/
293
+ next if matches.empty?
294
+
295
+ step, amount = matches[0]
296
+
297
+ if ['done', 'error'].include? amount
298
+ # step is done, explicitly print result and newline
299
+ redisplay "#{@last_progress[0].capitalize}... #{amount}\n"
300
+ end
301
+
302
+ # store progress, last one in the logs will get displayed
303
+ step = step_map[step] || step
304
+ @last_progress = [step, amount]
305
+ end
306
+
307
+ step, amount = @last_progress
308
+ unless ['done', 'error'].include? amount
309
+ redisplay "#{step.capitalize}... #{amount} #{spinner(@ticks)}"
310
+ end
311
+ end
312
+ end
313
+
314
+ private
315
+
316
+ def no_backups_error!
317
+ error("No backups. Capture one with `azuki pgbackups:capture`.")
318
+ end
319
+
320
+ # lists all types of backups ('to_name' attribute)
321
+ #
322
+ # Useful when one doesn't care if a backup is of a particular
323
+ # kind, but wants to know what backups of any kind exist.
324
+ #
325
+ def backup_types
326
+ %w[BACKUP DAILY_SCHEDULED_BACKUP HOURLY_SCHEDULED_BACKUP AUTO_SCHEDULED_BACKUP]
327
+ end
328
+ end
329
+ end
@@ -0,0 +1,110 @@
1
+ require "azuki/command/base"
2
+
3
+ module Azuki::Command
4
+
5
+ # manage plugins to the azuki gem
6
+ class Plugins < Base
7
+
8
+ # plugins
9
+ #
10
+ # list installed plugins
11
+ #
12
+ #Example:
13
+ #
14
+ # $ azuki plugins
15
+ # === Installed Plugins
16
+ # azuki-accounts
17
+ #
18
+ def index
19
+ validate_arguments!
20
+
21
+ plugins = ::Azuki::Plugin.list
22
+
23
+ if plugins.length > 0
24
+ styled_header("Installed Plugins")
25
+ styled_array(plugins)
26
+ else
27
+ display("You have no installed plugins.")
28
+ end
29
+ end
30
+
31
+ # plugins:install URL
32
+ #
33
+ # install a plugin
34
+ #
35
+ #Example:
36
+ #
37
+ # $ azuki plugins:install https://github.com/ddollar/azuki-accounts.git
38
+ # Installing azuki-accounts... done
39
+ #
40
+ def install
41
+ plugin = Azuki::Plugin.new(shift_argument)
42
+ validate_arguments!
43
+
44
+ action("Installing #{plugin.name}") do
45
+ if plugin.install
46
+ unless Azuki::Plugin.load_plugin(plugin.name)
47
+ plugin.uninstall
48
+ exit(1)
49
+ end
50
+ else
51
+ error("Could not install #{plugin.name}. Please check the URL and try again.")
52
+ end
53
+ end
54
+ end
55
+
56
+ # plugins:uninstall PLUGIN
57
+ #
58
+ # uninstall a plugin
59
+ #
60
+ #Example:
61
+ #
62
+ # $ azuki plugins:uninstall azuki-accounts
63
+ # Uninstalling azuki-accounts... done
64
+ #
65
+ def uninstall
66
+ plugin = Azuki::Plugin.new(shift_argument)
67
+ validate_arguments!
68
+
69
+ action("Uninstalling #{plugin.name}") do
70
+ plugin.uninstall
71
+ end
72
+ end
73
+
74
+ # plugins:update [PLUGIN]
75
+ #
76
+ # updates all plugins or a single plugin by name
77
+ #
78
+ #Example:
79
+ #
80
+ # $ azuki plugins:update
81
+ # Updating azuki-accounts... done
82
+ #
83
+ # $ azuki plugins:update azuki-accounts
84
+ # Updating azuki-accounts... done
85
+ #
86
+ def update
87
+ plugins = if plugin = shift_argument
88
+ [plugin]
89
+ else
90
+ ::Azuki::Plugin.list
91
+ end
92
+ validate_arguments!
93
+
94
+ plugins.each do |plugin|
95
+ begin
96
+ action("Updating #{plugin}") do
97
+ begin
98
+ Azuki::Plugin.new(plugin).update
99
+ rescue Azuki::Plugin::ErrorUpdatingSymlinkPlugin
100
+ status "skipped symlink"
101
+ end
102
+ end
103
+ rescue SystemExit
104
+ # ignore so that other plugins still update
105
+ end
106
+ end
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,232 @@
1
+ require "azuki/command/base"
2
+
3
+ # manage processes (dynos, workers)
4
+ #
5
+ class Azuki::Command::Ps < Azuki::Command::Base
6
+
7
+ # ps:dynos [QTY]
8
+ #
9
+ # DEPRECATED: use `azuki ps:scale dynos=N`
10
+ #
11
+ # scale to QTY web processes
12
+ #
13
+ # if QTY is not specified, display the number of web processes currently running
14
+ #
15
+ #Example:
16
+ #
17
+ # $ azuki ps:dynos 3
18
+ # Scaling dynos... done, now running 3
19
+ #
20
+ def dynos
21
+ # deprecation notice added to v2.21.3 on 03/16/12
22
+ display("~ `azuki ps:dynos QTY` has been deprecated and replaced with `azuki ps:scale dynos=QTY`")
23
+
24
+ dynos = shift_argument
25
+ validate_arguments!
26
+
27
+ if dynos
28
+ action("Scaling dynos") do
29
+ new_dynos = api.put_dynos(app, dynos).body["dynos"]
30
+ status("now running #{new_dynos}")
31
+ end
32
+ else
33
+ app_data = api.get_app(app).body
34
+ if app_data["stack"] == "cedar"
35
+ raise(Azuki::Command::CommandFailed, "For Cedar apps, use `azuki ps`")
36
+ else
37
+ display("#{app} is running #{quantify("dyno", app_data["dynos"])}")
38
+ end
39
+ end
40
+ end
41
+
42
+ alias_command "dynos", "ps:dynos"
43
+
44
+ # ps:workers [QTY]
45
+ #
46
+ # DEPRECATED: use `azuki ps:scale workers=N`
47
+ #
48
+ # scale to QTY background processes
49
+ #
50
+ # if QTY is not specified, display the number of background processes currently running
51
+ #
52
+ #Example:
53
+ #
54
+ # $ azuki ps:dynos 3
55
+ # Scaling workers... done, now running 3
56
+ #
57
+ def workers
58
+ # deprecation notice added to v2.21.3 on 03/16/12
59
+ display("~ `azuki ps:workers QTY` has been deprecated and replaced with `azuki ps:scale workers=QTY`")
60
+
61
+ workers = shift_argument
62
+ validate_arguments!
63
+
64
+ if workers
65
+ action("Scaling workers") do
66
+ new_workers = api.put_workers(app, workers).body["workers"]
67
+ status("now running #{new_workers}")
68
+ end
69
+ else
70
+ app_data = api.get_app(app).body
71
+ if app_data["stack"] == "cedar"
72
+ raise(Azuki::Command::CommandFailed, "For Cedar apps, use `azuki ps`")
73
+ else
74
+ display("#{app} is running #{quantify("worker", app_data["workers"])}")
75
+ end
76
+ end
77
+ end
78
+
79
+ alias_command "workers", "ps:workers"
80
+
81
+ # ps
82
+ #
83
+ # list processes for an app
84
+ #
85
+ #Example:
86
+ #
87
+ # $ azuki ps
88
+ # === run: one-off processes
89
+ # run.1: up for 5m: `bash`
90
+ #
91
+ # === web: `bundle exec thin start -p $PORT`
92
+ # web.1: created for 30s
93
+ #
94
+ def index
95
+ validate_arguments!
96
+ processes = api.get_ps(app).body
97
+
98
+ processes_by_command = Hash.new {|hash,key| hash[key] = []}
99
+ processes.each do |process|
100
+ name = process["process"].split(".").first
101
+ elapsed = time_ago(Time.now - process['elapsed'])
102
+
103
+ if name == "run"
104
+ key = "run: one-off processes"
105
+ item = "%s: %s %s: `%s`" % [ process["process"], process["state"], elapsed, process["command"] ]
106
+ else
107
+ key = "#{name}: `#{process["command"]}`"
108
+ item = "%s: %s %s" % [ process["process"], process["state"], elapsed ]
109
+ end
110
+
111
+ processes_by_command[key] << item
112
+ end
113
+
114
+ processes_by_command.keys.each do |key|
115
+ processes_by_command[key] = processes_by_command[key].sort do |x,y|
116
+ x.match(/\.(\d+):/).captures.first.to_i <=> y.match(/\.(\d+):/).captures.first.to_i
117
+ end
118
+ end
119
+
120
+ processes_by_command.keys.sort.each do |key|
121
+ styled_header(key)
122
+ styled_array(processes_by_command[key], :sort => false)
123
+ end
124
+ end
125
+
126
+ # ps:restart [PROCESS]
127
+ #
128
+ # restart an app process
129
+ #
130
+ # if PROCESS is not specified, restarts all processes on the app
131
+ #
132
+ #Examples:
133
+ #
134
+ # $ azuki ps:restart web.1
135
+ # Restarting web.1 process... done
136
+ #
137
+ # $ azuki ps:restart web
138
+ # Restarting web processes... done
139
+ #
140
+ # $ azuki ps:restart
141
+ # Restarting processes... done
142
+ #
143
+ def restart
144
+ process = shift_argument
145
+ validate_arguments!
146
+
147
+ message, options = case process
148
+ when NilClass
149
+ ["Restarting processes", {}]
150
+ when /.+\..+/
151
+ ps = args.first
152
+ ["Restarting #{ps} process", { :ps => ps }]
153
+ else
154
+ type = args.first
155
+ ["Restarting #{type} processes", { :type => type }]
156
+ end
157
+
158
+ action(message) do
159
+ api.post_ps_restart(app, options)
160
+ end
161
+ end
162
+
163
+ alias_command "restart", "ps:restart"
164
+
165
+ # ps:scale PROCESS1=AMOUNT1 [PROCESS2=AMOUNT2 ...]
166
+ #
167
+ # scale processes by the given amount
168
+ #
169
+ #Examples:
170
+ #
171
+ # $ azuki ps:scale web=3 worker+1
172
+ # Scaling web processes... done, now running 3
173
+ # Scaling worker processes... done, now running 1
174
+ #
175
+ def scale
176
+ changes = {}
177
+ args.each do |arg|
178
+ if arg =~ /^([a-zA-Z0-9_]+)([=+-]\d+)$/
179
+ changes[$1] = $2
180
+ end
181
+ end
182
+
183
+ if changes.empty?
184
+ error("Usage: azuki ps:scale PROCESS1=AMOUNT1 [PROCESS2=AMOUNT2 ...]\nMust specify PROCESS and AMOUNT to scale.")
185
+ end
186
+
187
+ changes.keys.sort.each do |process|
188
+ amount = changes[process]
189
+ action("Scaling #{process} processes") do
190
+ amount.gsub!("=", "")
191
+ new_qty = api.post_ps_scale(app, process, amount).body
192
+ status("now running #{new_qty}")
193
+ end
194
+ end
195
+ end
196
+
197
+ alias_command "scale", "ps:scale"
198
+
199
+ # ps:stop PROCESS
200
+ #
201
+ # stop an app process
202
+ #
203
+ # Examples:
204
+ #
205
+ # $ azuki stop run.3
206
+ # Stopping run.3 process... done
207
+ #
208
+ # $ azuki stop run
209
+ # Stopping run processes... done
210
+ #
211
+ def stop
212
+ process = shift_argument
213
+ validate_arguments!
214
+
215
+ message, options = case process
216
+ when NilClass
217
+ error("Usage: azuki ps:stop PROCESS\nMust specify PROCESS to stop.")
218
+ when /.+\..+/
219
+ ps = args.first
220
+ ["Stopping #{ps} process", { :ps => ps }]
221
+ else
222
+ type = args.first
223
+ ["Stopping #{type} processes", { :type => type }]
224
+ end
225
+
226
+ action(message) do
227
+ api.post_ps_stop(app, options)
228
+ end
229
+ end
230
+
231
+ alias_command "stop", "ps:stop"
232
+ end