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.
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,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