azuki 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +71 -0
- data/bin/azuki +17 -0
- data/data/cacert.pem +3988 -0
- data/lib/azuki.rb +17 -0
- data/lib/azuki/auth.rb +339 -0
- data/lib/azuki/cli.rb +38 -0
- data/lib/azuki/client.rb +764 -0
- data/lib/azuki/client/azuki_postgresql.rb +141 -0
- data/lib/azuki/client/cisaurus.rb +26 -0
- data/lib/azuki/client/pgbackups.rb +113 -0
- data/lib/azuki/client/rendezvous.rb +108 -0
- data/lib/azuki/client/ssl_endpoint.rb +25 -0
- data/lib/azuki/command.rb +294 -0
- data/lib/azuki/command/account.rb +23 -0
- data/lib/azuki/command/accounts.rb +34 -0
- data/lib/azuki/command/addons.rb +305 -0
- data/lib/azuki/command/apps.rb +393 -0
- data/lib/azuki/command/auth.rb +86 -0
- data/lib/azuki/command/base.rb +230 -0
- data/lib/azuki/command/certs.rb +209 -0
- data/lib/azuki/command/config.rb +137 -0
- data/lib/azuki/command/db.rb +218 -0
- data/lib/azuki/command/domains.rb +85 -0
- data/lib/azuki/command/drains.rb +46 -0
- data/lib/azuki/command/fork.rb +164 -0
- data/lib/azuki/command/git.rb +64 -0
- data/lib/azuki/command/help.rb +179 -0
- data/lib/azuki/command/keys.rb +115 -0
- data/lib/azuki/command/labs.rb +147 -0
- data/lib/azuki/command/logs.rb +45 -0
- data/lib/azuki/command/maintenance.rb +61 -0
- data/lib/azuki/command/pg.rb +269 -0
- data/lib/azuki/command/pgbackups.rb +329 -0
- data/lib/azuki/command/plugins.rb +110 -0
- data/lib/azuki/command/ps.rb +232 -0
- data/lib/azuki/command/regions.rb +22 -0
- data/lib/azuki/command/releases.rb +124 -0
- data/lib/azuki/command/run.rb +180 -0
- data/lib/azuki/command/sharing.rb +89 -0
- data/lib/azuki/command/ssl.rb +43 -0
- data/lib/azuki/command/stack.rb +62 -0
- data/lib/azuki/command/status.rb +51 -0
- data/lib/azuki/command/update.rb +47 -0
- data/lib/azuki/command/version.rb +23 -0
- data/lib/azuki/deprecated.rb +5 -0
- data/lib/azuki/deprecated/help.rb +38 -0
- data/lib/azuki/distribution.rb +9 -0
- data/lib/azuki/excon.rb +9 -0
- data/lib/azuki/helpers.rb +517 -0
- data/lib/azuki/helpers/azuki_postgresql.rb +165 -0
- data/lib/azuki/helpers/log_displayer.rb +70 -0
- data/lib/azuki/plugin.rb +163 -0
- data/lib/azuki/updater.rb +171 -0
- data/lib/azuki/version.rb +3 -0
- data/lib/vendor/azuki/okjson.rb +598 -0
- data/spec/azuki/auth_spec.rb +256 -0
- data/spec/azuki/client/azuki_postgresql_spec.rb +71 -0
- data/spec/azuki/client/pgbackups_spec.rb +43 -0
- data/spec/azuki/client/rendezvous_spec.rb +62 -0
- data/spec/azuki/client/ssl_endpoint_spec.rb +48 -0
- data/spec/azuki/client_spec.rb +564 -0
- data/spec/azuki/command/addons_spec.rb +601 -0
- data/spec/azuki/command/apps_spec.rb +351 -0
- data/spec/azuki/command/auth_spec.rb +38 -0
- data/spec/azuki/command/base_spec.rb +109 -0
- data/spec/azuki/command/certs_spec.rb +178 -0
- data/spec/azuki/command/config_spec.rb +144 -0
- data/spec/azuki/command/db_spec.rb +110 -0
- data/spec/azuki/command/domains_spec.rb +87 -0
- data/spec/azuki/command/drains_spec.rb +34 -0
- data/spec/azuki/command/fork_spec.rb +56 -0
- data/spec/azuki/command/git_spec.rb +144 -0
- data/spec/azuki/command/help_spec.rb +93 -0
- data/spec/azuki/command/keys_spec.rb +120 -0
- data/spec/azuki/command/labs_spec.rb +100 -0
- data/spec/azuki/command/logs_spec.rb +60 -0
- data/spec/azuki/command/maintenance_spec.rb +51 -0
- data/spec/azuki/command/pg_spec.rb +236 -0
- data/spec/azuki/command/pgbackups_spec.rb +307 -0
- data/spec/azuki/command/plugins_spec.rb +104 -0
- data/spec/azuki/command/ps_spec.rb +195 -0
- data/spec/azuki/command/releases_spec.rb +130 -0
- data/spec/azuki/command/run_spec.rb +83 -0
- data/spec/azuki/command/sharing_spec.rb +59 -0
- data/spec/azuki/command/stack_spec.rb +46 -0
- data/spec/azuki/command/status_spec.rb +48 -0
- data/spec/azuki/command/version_spec.rb +16 -0
- data/spec/azuki/command_spec.rb +211 -0
- data/spec/azuki/helpers/azuki_postgresql_spec.rb +155 -0
- data/spec/azuki/helpers_spec.rb +48 -0
- data/spec/azuki/plugin_spec.rb +172 -0
- data/spec/azuki/updater_spec.rb +44 -0
- data/spec/helper/legacy_help.rb +16 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +224 -0
- data/spec/support/display_message_matcher.rb +49 -0
- data/spec/support/openssl_mock_helper.rb +8 -0
- metadata +211 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
require "azuki/command/base"
|
2
|
+
|
3
|
+
# manage azuki account options
|
4
|
+
#
|
5
|
+
class Azuki::Command::Account < Azuki::Command::Base
|
6
|
+
|
7
|
+
# account:confirm_billing
|
8
|
+
#
|
9
|
+
# Confirm that your account can be billed at the end of the month
|
10
|
+
#
|
11
|
+
#Example:
|
12
|
+
#
|
13
|
+
# $ azuki account:confirm_billing
|
14
|
+
# This action will cause your account to be billed at the end of the month
|
15
|
+
# For more information, see http://docs.azukiapp.com/billing
|
16
|
+
# Are you sure you want to do this? (y/n)
|
17
|
+
#
|
18
|
+
def confirm_billing
|
19
|
+
validate_arguments!
|
20
|
+
Azuki::Helpers.confirm_billing
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
if Azuki::Plugin.list.include?('azuki-accounts')
|
2
|
+
|
3
|
+
require "azuki/command/base"
|
4
|
+
|
5
|
+
# manage multiple azuki accounts
|
6
|
+
#
|
7
|
+
class Azuki::Command::Accounts < Azuki::Command::Base
|
8
|
+
|
9
|
+
# accounts:default
|
10
|
+
# set a system-wide default account
|
11
|
+
def default
|
12
|
+
name = shift_argument
|
13
|
+
validate_arguments!
|
14
|
+
|
15
|
+
unless name
|
16
|
+
error("Please specify an account name.")
|
17
|
+
end
|
18
|
+
|
19
|
+
unless account_exists?(name)
|
20
|
+
error("That account does not exist.")
|
21
|
+
end
|
22
|
+
|
23
|
+
result = %x{ git config --global azuki.account #{name} }
|
24
|
+
|
25
|
+
# update netrc
|
26
|
+
Azuki::Auth.instance_variable_set(:@account, nil) # kill memoization
|
27
|
+
Azuki::Auth.credentials = [Azuki::Auth.user, Azuki::Auth.password]
|
28
|
+
Azuki::Auth.write_credentials
|
29
|
+
|
30
|
+
result
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,305 @@
|
|
1
|
+
require "azuki/command/base"
|
2
|
+
require "azuki/helpers/azuki_postgresql"
|
3
|
+
|
4
|
+
module Azuki::Command
|
5
|
+
|
6
|
+
# manage addon resources
|
7
|
+
#
|
8
|
+
class Addons < Base
|
9
|
+
|
10
|
+
include Azuki::Helpers::AzukiPostgresql
|
11
|
+
|
12
|
+
# addons
|
13
|
+
#
|
14
|
+
# list installed addons
|
15
|
+
#
|
16
|
+
def index
|
17
|
+
validate_arguments!
|
18
|
+
|
19
|
+
installed = api.get_addons(app).body
|
20
|
+
if installed.empty?
|
21
|
+
display("#{app} has no add-ons.")
|
22
|
+
else
|
23
|
+
available, pending = installed.partition { |a| a['configured'] }
|
24
|
+
|
25
|
+
unless available.empty?
|
26
|
+
styled_header("#{app} Configured Add-ons")
|
27
|
+
styled_array(available.map do |a|
|
28
|
+
[a['name'], a['attachment_name'] || '']
|
29
|
+
end)
|
30
|
+
end
|
31
|
+
|
32
|
+
unless pending.empty?
|
33
|
+
styled_header("#{app} Add-ons to Configure")
|
34
|
+
styled_array(pending.map do |a|
|
35
|
+
[a['name'], app_addon_url(a['name'])]
|
36
|
+
end)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# addons:list
|
42
|
+
#
|
43
|
+
# list all available addons
|
44
|
+
#
|
45
|
+
def list
|
46
|
+
addons = azuki.addons
|
47
|
+
if addons.empty?
|
48
|
+
display "No addons available currently"
|
49
|
+
else
|
50
|
+
partitioned_addons = partition_addons(addons)
|
51
|
+
partitioned_addons.each do |key, addons|
|
52
|
+
partitioned_addons[key] = format_for_display(addons)
|
53
|
+
end
|
54
|
+
display_object(partitioned_addons)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# addons:add ADDON
|
59
|
+
#
|
60
|
+
# install an addon
|
61
|
+
#
|
62
|
+
def add
|
63
|
+
configure_addon('Adding') do |addon, config|
|
64
|
+
azuki.install_addon(app, addon, config)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# addons:upgrade ADDON
|
69
|
+
#
|
70
|
+
# upgrade an existing addon
|
71
|
+
#
|
72
|
+
def upgrade
|
73
|
+
configure_addon('Upgrading to') do |addon, config|
|
74
|
+
azuki.upgrade_addon(app, addon, config)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# addons:downgrade ADDON
|
79
|
+
#
|
80
|
+
# downgrade an existing addon
|
81
|
+
#
|
82
|
+
def downgrade
|
83
|
+
configure_addon('Downgrading to') do |addon, config|
|
84
|
+
azuki.upgrade_addon(app, addon, config)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# addons:remove ADDON1 [ADDON2 ...]
|
89
|
+
#
|
90
|
+
# uninstall one or more addons
|
91
|
+
#
|
92
|
+
def remove
|
93
|
+
return unless confirm_command
|
94
|
+
|
95
|
+
args.each do |name|
|
96
|
+
messages = nil
|
97
|
+
action("Removing #{name} on #{app}") do
|
98
|
+
messages = addon_run { azuki.uninstall_addon(app, name, :confirm => app) }
|
99
|
+
end
|
100
|
+
display(messages[:attachment]) if messages[:attachment]
|
101
|
+
display(messages[:message]) if messages[:message]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# addons:docs ADDON
|
106
|
+
#
|
107
|
+
# open an addon's documentation in your browser
|
108
|
+
#
|
109
|
+
def docs
|
110
|
+
unless addon = shift_argument
|
111
|
+
error("Usage: azuki addons:docs ADDON\nMust specify ADDON to open docs for.")
|
112
|
+
end
|
113
|
+
validate_arguments!
|
114
|
+
|
115
|
+
addon_names = api.get_addons.body.map {|a| a['name']}
|
116
|
+
addon_types = addon_names.map {|name| name.split(':').first}.uniq
|
117
|
+
|
118
|
+
name_matches = addon_names.select {|name| name =~ /^#{addon}/}
|
119
|
+
type_matches = addon_types.select {|name| name =~ /^#{addon}/}
|
120
|
+
|
121
|
+
if name_matches.include?(addon) || type_matches.include?(addon)
|
122
|
+
type_matches = [addon]
|
123
|
+
end
|
124
|
+
|
125
|
+
case type_matches.length
|
126
|
+
when 0 then
|
127
|
+
error([
|
128
|
+
"`#{addon}` is not a azuki add-on.",
|
129
|
+
suggestion(addon, addon_names + addon_types),
|
130
|
+
"See `azuki addons:list` for all available addons."
|
131
|
+
].compact.join("\n"))
|
132
|
+
when 1
|
133
|
+
addon_type = type_matches.first
|
134
|
+
launchy("Opening #{addon_type} docs", addon_docs_url(addon_type))
|
135
|
+
else
|
136
|
+
error("Ambiguous addon name: #{addon}\nPerhaps you meant #{name_matches[0...-1].map {|match| "`#{match}`"}.join(', ')} or `#{name_matches.last}`.\n")
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# addons:open ADDON
|
141
|
+
#
|
142
|
+
# open an addon's dashboard in your browser
|
143
|
+
#
|
144
|
+
def open
|
145
|
+
unless addon = shift_argument
|
146
|
+
error("Usage: azuki addons:open ADDON\nMust specify ADDON to open.")
|
147
|
+
end
|
148
|
+
validate_arguments!
|
149
|
+
|
150
|
+
app_addons = api.get_addons(app).body.map {|a| a['name']}
|
151
|
+
matches = app_addons.select {|a| a =~ /^#{addon}/}.sort
|
152
|
+
|
153
|
+
case matches.length
|
154
|
+
when 0 then
|
155
|
+
addon_names = api.get_addons.body.map {|a| a['name']}
|
156
|
+
if addon_names.any? {|name| name =~ /^#{addon}/}
|
157
|
+
error("Addon not installed: #{addon}")
|
158
|
+
else
|
159
|
+
error([
|
160
|
+
"`#{addon}` is not a azuki add-on.",
|
161
|
+
suggestion(addon, addon_names + addon_names.map {|name| name.split(':').first}.uniq),
|
162
|
+
"See `azuki addons:list` for all available addons."
|
163
|
+
].compact.join("\n"))
|
164
|
+
end
|
165
|
+
when 1 then
|
166
|
+
addon_to_open = matches.first
|
167
|
+
launchy("Opening #{addon_to_open} for #{app}", app_addon_url(addon_to_open))
|
168
|
+
else
|
169
|
+
error("Ambiguous addon name: #{addon}\nPerhaps you meant #{matches[0...-1].map {|match| "`#{match}`"}.join(', ')} or `#{matches.last}`.\n")
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
def addon_docs_url(addon)
|
176
|
+
"https://devcenter.#{azuki.host}/articles/#{addon.split(':').first}"
|
177
|
+
end
|
178
|
+
|
179
|
+
def app_addon_url(addon)
|
180
|
+
"https://api.#{azuki.host}/apps/#{app}/addons/#{addon}"
|
181
|
+
end
|
182
|
+
|
183
|
+
def partition_addons(addons)
|
184
|
+
addons.group_by{ |a| (a["state"] == "public" ? "available" : a["state"]) }
|
185
|
+
end
|
186
|
+
|
187
|
+
def format_for_display(addons)
|
188
|
+
grouped = addons.inject({}) do |base, addon|
|
189
|
+
group, short = addon['name'].split(':')
|
190
|
+
base[group] ||= []
|
191
|
+
base[group] << addon.merge('short' => short)
|
192
|
+
base
|
193
|
+
end
|
194
|
+
grouped.keys.sort.map do |name|
|
195
|
+
addons = grouped[name]
|
196
|
+
row = name.dup
|
197
|
+
if addons.any? { |a| a['short'] }
|
198
|
+
row << ':'
|
199
|
+
size = row.size
|
200
|
+
stop = false
|
201
|
+
row << addons.map { |a| a['short'] }.compact.sort.map do |short|
|
202
|
+
size += short.size
|
203
|
+
if size < 31
|
204
|
+
short
|
205
|
+
else
|
206
|
+
stop = true
|
207
|
+
nil
|
208
|
+
end
|
209
|
+
end.compact.join(', ')
|
210
|
+
row << '...' if stop
|
211
|
+
end
|
212
|
+
row
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def addon_run
|
217
|
+
response = yield
|
218
|
+
|
219
|
+
if response
|
220
|
+
price = "(#{ response['price'] })" if response['price']
|
221
|
+
|
222
|
+
if response['message'] =~ /(Attached as [A-Z0-9_]+)\n(.*)/m
|
223
|
+
attachment = $1
|
224
|
+
message = $2
|
225
|
+
else
|
226
|
+
attachment = nil
|
227
|
+
message = response['message']
|
228
|
+
end
|
229
|
+
|
230
|
+
begin
|
231
|
+
release = api.get_release(app, 'current').body
|
232
|
+
release = release['name']
|
233
|
+
rescue Azuki::API::Errors::Error
|
234
|
+
release = nil
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
status [ release, price ].compact.join(' ')
|
239
|
+
{ :attachment => attachment, :message => message }
|
240
|
+
rescue RestClient::ResourceNotFound => e
|
241
|
+
error Azuki::Command.extract_error(e.http_body) {
|
242
|
+
e.http_body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found"
|
243
|
+
}
|
244
|
+
rescue RestClient::Locked => ex
|
245
|
+
raise
|
246
|
+
rescue RestClient::RequestFailed => e
|
247
|
+
retry if e.http_code == 402 && confirm_billing
|
248
|
+
error Azuki::Command.extract_error(e.http_body)
|
249
|
+
end
|
250
|
+
|
251
|
+
def configure_addon(label, &install_or_upgrade)
|
252
|
+
addon = args.shift
|
253
|
+
raise CommandFailed.new("Missing add-on name") if addon.nil? || ["--fork", "--follow"].include?(addon)
|
254
|
+
|
255
|
+
config = parse_options(args)
|
256
|
+
config.merge!(:confirm => app) if app == options[:confirm]
|
257
|
+
raise CommandFailed.new("Unexpected arguments: #{args.join(' ')}") unless args.empty?
|
258
|
+
|
259
|
+
hpg_translate_fork_and_follow(addon, config)
|
260
|
+
|
261
|
+
messages = nil
|
262
|
+
action("#{label} #{addon} on #{app}") do
|
263
|
+
messages = addon_run { install_or_upgrade.call(addon, config) }
|
264
|
+
end
|
265
|
+
display(messages[:attachment]) unless messages[:attachment].to_s.strip == ""
|
266
|
+
display(messages[:message]) unless messages[:message].to_s.strip == ""
|
267
|
+
|
268
|
+
display("Use `azuki addons:docs #{addon}` to view documentation.")
|
269
|
+
end
|
270
|
+
|
271
|
+
#this will clean up when we officially deprecate
|
272
|
+
def parse_options(args)
|
273
|
+
config = {}
|
274
|
+
deprecated_args = []
|
275
|
+
flag = /^--/
|
276
|
+
|
277
|
+
args.size.times do
|
278
|
+
break if args.empty?
|
279
|
+
peek = args.first
|
280
|
+
next unless peek && (peek.match(flag) || peek.match(/=/))
|
281
|
+
arg = args.shift
|
282
|
+
peek = args.first
|
283
|
+
key = arg
|
284
|
+
if key.match(/=/)
|
285
|
+
deprecated_args << key unless key.match(flag)
|
286
|
+
key, value = key.split('=', 2)
|
287
|
+
elsif peek.nil? || peek.match(flag)
|
288
|
+
value = true
|
289
|
+
else
|
290
|
+
value = args.shift
|
291
|
+
end
|
292
|
+
value = true if value == 'true'
|
293
|
+
config[key.sub(flag, '')] = value
|
294
|
+
|
295
|
+
if !deprecated_args.empty?
|
296
|
+
out_string = deprecated_args.map{|a| "--#{a}"}.join(' ')
|
297
|
+
display("Warning: non-unix style params have been deprecated, use #{out_string} instead")
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
config
|
302
|
+
end
|
303
|
+
|
304
|
+
end
|
305
|
+
end
|
@@ -0,0 +1,393 @@
|
|
1
|
+
require "azuki/command/base"
|
2
|
+
|
3
|
+
# manage apps (create, destroy)
|
4
|
+
#
|
5
|
+
class Azuki::Command::Apps < Azuki::Command::Base
|
6
|
+
|
7
|
+
# apps
|
8
|
+
#
|
9
|
+
# list your apps
|
10
|
+
#
|
11
|
+
#Example:
|
12
|
+
#
|
13
|
+
# $ azuki apps
|
14
|
+
# === My Apps
|
15
|
+
# example
|
16
|
+
# example2
|
17
|
+
#
|
18
|
+
# === Collaborated Apps
|
19
|
+
# theirapp other@owner.name
|
20
|
+
#
|
21
|
+
def index
|
22
|
+
validate_arguments!
|
23
|
+
apps = api.get_apps.body
|
24
|
+
unless apps.empty?
|
25
|
+
my_apps, collaborated_apps = apps.partition do |app|
|
26
|
+
app["owner_email"] == Azuki::Auth.user
|
27
|
+
end
|
28
|
+
|
29
|
+
unless my_apps.empty?
|
30
|
+
non_legacy_apps = my_apps.select do |app|
|
31
|
+
app["tier"] != "legacy"
|
32
|
+
end
|
33
|
+
|
34
|
+
unless non_legacy_apps.empty?
|
35
|
+
production_basic_apps, dev_legacy_apps = my_apps.partition do |app|
|
36
|
+
["production", "basic"].include?(app["tier"])
|
37
|
+
end
|
38
|
+
|
39
|
+
unless production_basic_apps.empty?
|
40
|
+
styled_header("Basic & Production Apps")
|
41
|
+
styled_array(production_basic_apps.map { |app| regionized_app_name(app) })
|
42
|
+
end
|
43
|
+
|
44
|
+
unless dev_legacy_apps.empty?
|
45
|
+
styled_header("Dev & Legacy Apps")
|
46
|
+
styled_array(dev_legacy_apps.map { |app| regionized_app_name(app) })
|
47
|
+
end
|
48
|
+
else
|
49
|
+
styled_header("My Apps")
|
50
|
+
styled_array(my_apps.map { |app| regionized_app_name(app) })
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
unless collaborated_apps.empty?
|
55
|
+
styled_header("Collaborated Apps")
|
56
|
+
styled_array(collaborated_apps.map { |app| [regionized_app_name(app), app["owner_email"]] })
|
57
|
+
end
|
58
|
+
else
|
59
|
+
display("You have no apps.")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
alias_command "list", "apps"
|
64
|
+
|
65
|
+
# apps:info
|
66
|
+
#
|
67
|
+
# show detailed app information
|
68
|
+
#
|
69
|
+
# -s, --shell # output more shell friendly key/value pairs
|
70
|
+
#
|
71
|
+
#Examples:
|
72
|
+
#
|
73
|
+
# $ azuki apps:info
|
74
|
+
# === example
|
75
|
+
# Git URL: git@azukiapp.com:example.git
|
76
|
+
# Repo Size: 5M
|
77
|
+
# ...
|
78
|
+
#
|
79
|
+
# $ azuki apps:info --shell
|
80
|
+
# git_url=git@azukiapp.com:example.git
|
81
|
+
# repo_size=5000000
|
82
|
+
# ...
|
83
|
+
#
|
84
|
+
def info
|
85
|
+
validate_arguments!
|
86
|
+
app_data = api.get_app(app).body
|
87
|
+
|
88
|
+
unless options[:shell]
|
89
|
+
styled_header(app_data["name"])
|
90
|
+
end
|
91
|
+
|
92
|
+
addons_data = api.get_addons(app).body.map {|addon| addon['name']}.sort
|
93
|
+
collaborators_data = api.get_collaborators(app).body.map {|collaborator| collaborator["email"]}.sort
|
94
|
+
collaborators_data.reject! {|email| email == app_data["owner_email"]}
|
95
|
+
|
96
|
+
if options[:shell]
|
97
|
+
if app_data['domain_name']
|
98
|
+
app_data['domain_name'] = app_data['domain_name']['domain']
|
99
|
+
end
|
100
|
+
unless addons_data.empty?
|
101
|
+
app_data['addons'] = addons_data.join(',')
|
102
|
+
end
|
103
|
+
unless collaborators_data.empty?
|
104
|
+
app_data['collaborators'] = collaborators_data.join(',')
|
105
|
+
end
|
106
|
+
app_data.keys.sort_by { |a| a.to_s }.each do |key|
|
107
|
+
hputs("#{key}=#{app_data[key]}")
|
108
|
+
end
|
109
|
+
else
|
110
|
+
data = {}
|
111
|
+
|
112
|
+
unless addons_data.empty?
|
113
|
+
data["Addons"] = addons_data
|
114
|
+
end
|
115
|
+
|
116
|
+
data["Collaborators"] = collaborators_data
|
117
|
+
|
118
|
+
if app_data["create_status"] && app_data["create_status"] != "complete"
|
119
|
+
data["Create Status"] = app_data["create_status"]
|
120
|
+
end
|
121
|
+
|
122
|
+
if app_data["cron_finished_at"]
|
123
|
+
data["Cron Finished At"] = format_date(app_data["cron_finished_at"])
|
124
|
+
end
|
125
|
+
|
126
|
+
if app_data["cron_next_run"]
|
127
|
+
data["Cron Next Run"] = format_date(app_data["cron_next_run"])
|
128
|
+
end
|
129
|
+
|
130
|
+
if app_data["database_size"]
|
131
|
+
data["Database Size"] = format_bytes(app_data["database_size"])
|
132
|
+
end
|
133
|
+
|
134
|
+
data["Git URL"] = app_data["git_url"]
|
135
|
+
|
136
|
+
if app_data["database_tables"]
|
137
|
+
data["Database Size"].gsub!('(empty)', '0K') + " in #{quantify("table", app_data["database_tables"])}"
|
138
|
+
end
|
139
|
+
|
140
|
+
if app_data["dyno_hours"].is_a?(Hash)
|
141
|
+
data["Dyno Hours"] = app_data["dyno_hours"].keys.map do |type|
|
142
|
+
"%s - %0.2f dyno-hours" % [ type.to_s.capitalize, app_data["dyno_hours"][type] ]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
data["Owner Email"] = app_data["owner_email"]
|
147
|
+
|
148
|
+
if app_data["region"]
|
149
|
+
data["Region"] = app_data["region"]
|
150
|
+
end
|
151
|
+
|
152
|
+
if app_data["repo_size"]
|
153
|
+
data["Repo Size"] = format_bytes(app_data["repo_size"])
|
154
|
+
end
|
155
|
+
|
156
|
+
if app_data["slug_size"]
|
157
|
+
data["Slug Size"] = format_bytes(app_data["slug_size"])
|
158
|
+
end
|
159
|
+
|
160
|
+
data["Stack"] = app_data["stack"]
|
161
|
+
if data["Stack"] != "cedar"
|
162
|
+
data.merge!("Dynos" => app_data["dynos"], "Workers" => app_data["workers"])
|
163
|
+
end
|
164
|
+
|
165
|
+
data["Web URL"] = app_data["web_url"]
|
166
|
+
|
167
|
+
if app_data["tier"]
|
168
|
+
data["Tier"] = app_data["tier"].capitalize
|
169
|
+
end
|
170
|
+
|
171
|
+
styled_hash(data)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
alias_command "info", "apps:info"
|
176
|
+
|
177
|
+
# apps:create [NAME]
|
178
|
+
#
|
179
|
+
# create a new app
|
180
|
+
#
|
181
|
+
# --addons ADDONS # a comma-delimited list of addons to install
|
182
|
+
# -b, --buildpack BUILDPACK # a buildpack url to use for this app
|
183
|
+
# -n, --no-remote # don't create a git remote
|
184
|
+
# -r, --remote REMOTE # the git remote to create, default "azuki"
|
185
|
+
# -s, --stack STACK # the stack on which to create the app
|
186
|
+
# --region REGION # HIDDEN: specify region for this app to run on
|
187
|
+
# -t, --tier TIER # HIDDEN: the tier for this app
|
188
|
+
#
|
189
|
+
#Examples:
|
190
|
+
#
|
191
|
+
# $ azuki apps:create
|
192
|
+
# Creating floating-dragon-42... done, stack is cedar
|
193
|
+
# http://floating-dragon-42.azukiapp.com/ | git@azukiapp.com:floating-dragon-42.git
|
194
|
+
#
|
195
|
+
# $ azuki apps:create -s bamboo
|
196
|
+
# Creating floating-dragon-42... done, stack is bamboo-mri-1.9.2
|
197
|
+
# http://floating-dragon-42.azukiapp.com/ | git@azukiapp.com:floating-dragon-42.git
|
198
|
+
#
|
199
|
+
# # specify a name
|
200
|
+
# $ azuki apps:create example
|
201
|
+
# Creating example... done, stack is cedar
|
202
|
+
# http://example.azukiapp.com/ | git@azukiapp.com:example.git
|
203
|
+
#
|
204
|
+
# # create a staging app
|
205
|
+
# $ azuki apps:create example-staging --remote staging
|
206
|
+
#
|
207
|
+
def create
|
208
|
+
name = shift_argument || options[:app] || ENV['AZUKI_APP']
|
209
|
+
validate_arguments!
|
210
|
+
|
211
|
+
info = api.post_app({
|
212
|
+
"name" => name,
|
213
|
+
"region" => options[:region],
|
214
|
+
"stack" => options[:stack],
|
215
|
+
"tier" => options[:tier]
|
216
|
+
}).body
|
217
|
+
begin
|
218
|
+
action("Creating #{info['name']}") do
|
219
|
+
if info['create_status'] == 'creating'
|
220
|
+
Timeout::timeout(options[:timeout].to_i) do
|
221
|
+
loop do
|
222
|
+
break if api.get_app(info['name']).body['create_status'] == 'complete'
|
223
|
+
sleep 1
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
if info['region']
|
228
|
+
status("region is #{info['region']}")
|
229
|
+
else
|
230
|
+
status("stack is #{info['stack']}")
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
(options[:addons] || "").split(",").each do |addon|
|
235
|
+
addon.strip!
|
236
|
+
action("Adding #{addon} to #{info["name"]}") do
|
237
|
+
api.post_addon(info["name"], addon)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
if buildpack = options[:buildpack]
|
242
|
+
api.put_config_vars(info["name"], "BUILDPACK_URL" => buildpack)
|
243
|
+
display("BUILDPACK_URL=#{buildpack}")
|
244
|
+
end
|
245
|
+
|
246
|
+
hputs([ info["web_url"], info["git_url"] ].join(" | "))
|
247
|
+
rescue Timeout::Error
|
248
|
+
hputs("Timed Out! Run `azuki status` to check for known platform issues.")
|
249
|
+
end
|
250
|
+
|
251
|
+
unless options[:no_remote].is_a? FalseClass
|
252
|
+
create_git_remote(options[:remote] || "azuki", info["git_url"])
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
alias_command "create", "apps:create"
|
257
|
+
|
258
|
+
# apps:rename NEWNAME
|
259
|
+
#
|
260
|
+
# rename the app
|
261
|
+
#
|
262
|
+
#Example:
|
263
|
+
#
|
264
|
+
# $ azuki apps:rename example-newname
|
265
|
+
# http://example-newname.azukiapp.com/ | git@azukiapp.com:example-newname.git
|
266
|
+
# Git remote azuki updated
|
267
|
+
#
|
268
|
+
def rename
|
269
|
+
newname = shift_argument
|
270
|
+
if newname.nil? || newname.empty?
|
271
|
+
error("Usage: azuki apps:rename NEWNAME\nMust specify NEWNAME to rename.")
|
272
|
+
end
|
273
|
+
validate_arguments!
|
274
|
+
|
275
|
+
action("Renaming #{app} to #{newname}") do
|
276
|
+
api.put_app(app, "name" => newname)
|
277
|
+
end
|
278
|
+
|
279
|
+
app_data = api.get_app(newname).body
|
280
|
+
hputs([ app_data["web_url"], app_data["git_url"] ].join(" | "))
|
281
|
+
|
282
|
+
if remotes = git_remotes(Dir.pwd)
|
283
|
+
remotes.each do |remote_name, remote_app|
|
284
|
+
next if remote_app != app
|
285
|
+
git "remote rm #{remote_name}"
|
286
|
+
git "remote add #{remote_name} #{app_data["git_url"]}"
|
287
|
+
hputs("Git remote #{remote_name} updated")
|
288
|
+
end
|
289
|
+
else
|
290
|
+
hputs("Don't forget to update your Git remotes on any local checkouts.")
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
alias_command "rename", "apps:rename"
|
295
|
+
|
296
|
+
# apps:open
|
297
|
+
#
|
298
|
+
# open the app in a web browser
|
299
|
+
#
|
300
|
+
#Example:
|
301
|
+
#
|
302
|
+
# $ azuki apps:open
|
303
|
+
# Opening example... done
|
304
|
+
#
|
305
|
+
def open
|
306
|
+
validate_arguments!
|
307
|
+
|
308
|
+
app_data = api.get_app(app).body
|
309
|
+
launchy("Opening #{app}", app_data['web_url'])
|
310
|
+
end
|
311
|
+
|
312
|
+
alias_command "open", "apps:open"
|
313
|
+
|
314
|
+
# apps:destroy
|
315
|
+
#
|
316
|
+
# permanently destroy an app
|
317
|
+
#
|
318
|
+
#Example:
|
319
|
+
#
|
320
|
+
# $ azuki apps:destroy -a example --confirm example
|
321
|
+
# Destroying example (including all add-ons)... done
|
322
|
+
#
|
323
|
+
def destroy
|
324
|
+
@app = shift_argument || options[:app] || options[:confirm]
|
325
|
+
validate_arguments!
|
326
|
+
|
327
|
+
unless @app
|
328
|
+
error("Usage: azuki apps:destroy --app APP\nMust specify APP to destroy.")
|
329
|
+
end
|
330
|
+
|
331
|
+
api.get_app(@app) # fail fast if no access or doesn't exist
|
332
|
+
|
333
|
+
message = "WARNING: Potentially Destructive Action\nThis command will destroy #{@app} (including all add-ons)."
|
334
|
+
if confirm_command(@app, message)
|
335
|
+
action("Destroying #{@app} (including all add-ons)") do
|
336
|
+
api.delete_app(@app)
|
337
|
+
if remotes = git_remotes(Dir.pwd)
|
338
|
+
remotes.each do |remote_name, remote_app|
|
339
|
+
next if @app != remote_app
|
340
|
+
git "remote rm #{remote_name}"
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
alias_command "destroy", "apps:destroy"
|
348
|
+
alias_command "apps:delete", "apps:destroy"
|
349
|
+
|
350
|
+
# apps:upgrade TIER
|
351
|
+
#
|
352
|
+
# HIDDEN: upgrade an app's pricing tier
|
353
|
+
#
|
354
|
+
def upgrade
|
355
|
+
tier = shift_argument
|
356
|
+
error("Usage: azuki apps:upgrade TIER\nMust specify TIER to upgrade.") if tier.nil? || tier.empty?
|
357
|
+
validate_arguments!
|
358
|
+
|
359
|
+
action("Upgrading #{app} to #{tier}") do
|
360
|
+
api.put_app(app, "tier" => tier)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
alias_command "upgrade", "apps:upgrade"
|
365
|
+
|
366
|
+
# apps:downgrade TIER
|
367
|
+
#
|
368
|
+
# HIDDEN: downgrade an app's pricing tier
|
369
|
+
#
|
370
|
+
def downgrade
|
371
|
+
tier = shift_argument
|
372
|
+
error("Usage: azuki apps:downgrade TIER\nMust specify TIER to downgrade.") if tier.nil? || tier.empty?
|
373
|
+
validate_arguments!
|
374
|
+
|
375
|
+
action("Upgrading #{app} to #{tier}") do
|
376
|
+
api.put_app(app, "tier" => tier)
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
alias_command "downgrade", "apps:downgrade"
|
381
|
+
|
382
|
+
private
|
383
|
+
|
384
|
+
def regionized_app_name(app)
|
385
|
+
# temporary, show region for non-us apps
|
386
|
+
if app["region"] && app["region"] != 'us'
|
387
|
+
"#{app["name"]} (#{app["region"]})"
|
388
|
+
else
|
389
|
+
app["name"]
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
end
|