azuki 0.0.1
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.
- 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
|