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