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,147 @@
1
+ require "azuki/command/base"
2
+
3
+ # manage optional features
4
+ #
5
+ class Azuki::Command::Labs < Azuki::Command::Base
6
+
7
+ # labs
8
+ #
9
+ # list experimental features
10
+ #
11
+ #Example:
12
+ #
13
+ # === User Features (david@azukiapp.com)
14
+ # [+] dashboard Use Azuki Dashboard by default
15
+ #
16
+ # === App Features (glacial-retreat-5913)
17
+ # [ ] preboot Provide seamless web dyno deploys
18
+ # [ ] user-env-compile Add user config vars to the environment during slug compilation # $ azuki labs -a example
19
+ #
20
+ def index
21
+ validate_arguments!
22
+
23
+ user_features, app_features = api.get_features(app).body.sort_by do |feature|
24
+ feature["name"]
25
+ end.partition do |feature|
26
+ feature["kind"] == "user"
27
+ end
28
+
29
+ display_app = app || "no app specified"
30
+
31
+ styled_header "User Features (#{Azuki::Auth.user})"
32
+ display_features user_features
33
+ display
34
+ styled_header "App Features (#{display_app})"
35
+ display_features app_features
36
+ end
37
+
38
+ alias_command "labs:list", "labs"
39
+
40
+ # labs:info FEATURE
41
+ #
42
+ # displays additional information about FEATURE
43
+ #
44
+ #Example:
45
+ #
46
+ # $ azuki labs:info user_env_compile
47
+ # === user_env_compile
48
+ # Docs: http://devcenter.azukiapp.com/articles/labs-user-env-compile
49
+ # Summary: Add user config vars to the environment during slug compilation
50
+ #
51
+ def info
52
+ unless feature_name = shift_argument
53
+ error("Usage: azuki labs:info FEATURE\nMust specify FEATURE for info.")
54
+ end
55
+ validate_arguments!
56
+
57
+ feature_data = api.get_feature(feature_name, app).body
58
+ styled_header(feature_data['name'])
59
+ styled_hash({
60
+ 'Summary' => feature_data['summary'],
61
+ 'Docs' => feature_data['docs']
62
+ })
63
+ end
64
+
65
+ # labs:disable FEATURE
66
+ #
67
+ # disables an experimental feature
68
+ #
69
+ #Example:
70
+ #
71
+ # $ azuki labs:disable ninja-power
72
+ # Disabling ninja-power feature for me@example.org... done
73
+ #
74
+ def disable
75
+ feature_name = shift_argument
76
+ error "Usage: azuki labs:disable FEATURE\nMust specify FEATURE to disable." unless feature_name
77
+ validate_arguments!
78
+
79
+ feature = api.get_features(app).body.detect { |f| f["name"] == feature_name }
80
+ message = "Disabling #{feature_name} "
81
+
82
+ error "No such feature: #{feature_name}" unless feature
83
+
84
+ if feature["kind"] == "user"
85
+ message += "for #{Azuki::Auth.user}"
86
+ else
87
+ error "Must specify an app" unless app
88
+ message += "for #{app}"
89
+ end
90
+
91
+ action message do
92
+ api.delete_feature feature_name, app
93
+ end
94
+ end
95
+
96
+ # labs:enable FEATURE
97
+ #
98
+ # enables an experimental feature
99
+ #
100
+ #Example:
101
+ #
102
+ # $ azuki labs:enable ninja-power
103
+ # Enabling ninja-power feature for me@example.org... done
104
+ #
105
+ def enable
106
+ feature_name = shift_argument
107
+ error "Usage: azuki labs:enable FEATURE\nMust specify FEATURE to enable." unless feature_name
108
+ validate_arguments!
109
+
110
+ feature = api.get_features.body.detect { |f| f["name"] == feature_name }
111
+ message = "Enabling #{feature_name} "
112
+
113
+ error "No such feature: #{feature_name}" unless feature
114
+
115
+ if feature["kind"] == "user"
116
+ message += "for #{Azuki::Auth.user}"
117
+ else
118
+ error "Must specify an app" unless app
119
+ message += "for #{app}"
120
+ end
121
+
122
+ feature_data = action(message) do
123
+ api.post_feature(feature_name, app).body
124
+ end
125
+
126
+ display "WARNING: This feature is experimental and may change or be removed without notice."
127
+ display "For more information see: #{feature_data["docs"]}" if feature_data["docs"]
128
+ end
129
+
130
+ private
131
+
132
+ # app is not required for these commands, so rescue if there is none
133
+ def app
134
+ super
135
+ rescue Azuki::Command::CommandFailed
136
+ nil
137
+ end
138
+
139
+ def display_features(features)
140
+ longest_name = features.map { |f| f["name"].to_s.length }.sort.last
141
+ features.each do |feature|
142
+ toggle = feature["enabled"] ? "[+]" : "[ ]"
143
+ display "%s %-#{longest_name}s %s" % [ toggle, feature["name"], feature["summary"] ]
144
+ end
145
+ end
146
+
147
+ end
@@ -0,0 +1,45 @@
1
+ require "azuki/command/base"
2
+ require "azuki/helpers/log_displayer"
3
+
4
+ # display logs for an app
5
+ #
6
+ class Azuki::Command::Logs < Azuki::Command::Base
7
+
8
+ # logs
9
+ #
10
+ # display recent log output
11
+ #
12
+ # -n, --num NUM # the number of lines to display
13
+ # -p, --ps PS # only display logs from the given process
14
+ # -s, --source SOURCE # only display logs from the given source
15
+ # -t, --tail # continually stream logs
16
+ #
17
+ #Example:
18
+ #
19
+ # $ azuki logs
20
+ # 2012-01-01T12:00:00+00:00 azuki[api]: Config add EXAMPLE by email@example.com
21
+ # 2012-01-01T12:00:01+00:00 azuki[api]: Release v1 created by email@example.com
22
+ #
23
+ def index
24
+ validate_arguments!
25
+
26
+ opts = []
27
+ opts << "tail=1" if options[:tail]
28
+ opts << "num=#{options[:num]}" if options[:num]
29
+ opts << "ps=#{URI.encode(options[:ps])}" if options[:ps]
30
+ opts << "source=#{URI.encode(options[:source])}" if options[:source]
31
+
32
+ log_displayer = ::Azuki::Helpers::LogDisplayer.new(azuki, app, opts)
33
+ log_displayer.display_logs
34
+ end
35
+
36
+ # logs:drains
37
+ #
38
+ # DEPRECATED: use `azuki drains`
39
+ #
40
+ def drains
41
+ # deprecation notice added 09/30/2011
42
+ display("~ `azuki logs:drains` has been deprecated and replaced with `azuki drains`")
43
+ Azuki::Command::Drains.new.index
44
+ end
45
+ end
@@ -0,0 +1,61 @@
1
+ require "azuki/command/base"
2
+
3
+ # manage maintenance mode for an app
4
+ #
5
+ class Azuki::Command::Maintenance < Azuki::Command::Base
6
+
7
+ # maintenance
8
+ #
9
+ # display the current maintenance status of app
10
+ #
11
+ #Example:
12
+ #
13
+ # $ azuki maintenance
14
+ # off
15
+ #
16
+ def index
17
+ validate_arguments!
18
+
19
+ case api.get_app_maintenance(app).body['maintenance']
20
+ when true
21
+ display('on')
22
+ when false
23
+ display('off')
24
+ end
25
+ end
26
+
27
+ # maintenance:on
28
+ #
29
+ # put the app into maintenance mode
30
+ #
31
+ #Example:
32
+ #
33
+ # $ azuki maintenance:on
34
+ # Enabling maintenance mode for example
35
+ #
36
+ def on
37
+ validate_arguments!
38
+
39
+ action("Enabling maintenance mode for #{app}") do
40
+ api.post_app_maintenance(app, '1')
41
+ end
42
+ end
43
+
44
+ # maintenance:off
45
+ #
46
+ # take the app out of maintenance mode
47
+ #
48
+ #Example:
49
+ #
50
+ # $ azuki maintenance:off
51
+ # Disabling maintenance mode for example
52
+ #
53
+ def off
54
+ validate_arguments!
55
+
56
+ action("Disabling maintenance mode for #{app}") do
57
+ api.post_app_maintenance(app, '0')
58
+ end
59
+ end
60
+
61
+ end
@@ -0,0 +1,269 @@
1
+ require "azuki/client/azuki_postgresql"
2
+ require "azuki/command/base"
3
+ require "azuki/helpers/azuki_postgresql"
4
+
5
+ # manage azuki-postgresql databases
6
+ #
7
+ class Azuki::Command::Pg < Azuki::Command::Base
8
+
9
+ include Azuki::Helpers::AzukiPostgresql
10
+
11
+ # pg
12
+ #
13
+ # List databases for an app
14
+ #
15
+ def index
16
+ validate_arguments!
17
+
18
+ if hpg_databases_with_info.empty?
19
+ display("#{app} has no azuki-postgresql databases.")
20
+ else
21
+ hpg_databases_with_info.keys.sort.each do |name|
22
+ display_db name, hpg_databases_with_info[name]
23
+ end
24
+ end
25
+ end
26
+
27
+ # pg:info [DATABASE]
28
+ #
29
+ # -x, --extended # Show extended information
30
+ #
31
+ # Display database information
32
+ #
33
+ # If DATABASE is not specified, displays all databases
34
+ #
35
+ def info
36
+ db = shift_argument
37
+ validate_arguments!
38
+
39
+ if db
40
+ attachment = hpg_resolve(db)
41
+ display_db attachment.display_name, hpg_info(attachment, options[:extended])
42
+ else
43
+ index
44
+ end
45
+ end
46
+
47
+ # pg:promote DATABASE
48
+ #
49
+ # Sets DATABASE as your DATABASE_URL
50
+ #
51
+ def promote
52
+ unless db = shift_argument
53
+ error("Usage: azuki pg:promote DATABASE\nMust specify DATABASE to promote.")
54
+ end
55
+ validate_arguments!
56
+
57
+ attachment = hpg_resolve(db)
58
+
59
+ action "Promoting #{attachment.display_name} to DATABASE_URL" do
60
+ hpg_promote(attachment.url)
61
+ end
62
+ end
63
+
64
+ # pg:psql [DATABASE]
65
+ #
66
+ # Open a psql shell to the database
67
+ #
68
+ # defaults to DATABASE_URL databases if no DATABASE is specified
69
+ #
70
+ def psql
71
+ attachment = hpg_resolve(shift_argument, "DATABASE_URL")
72
+ validate_arguments!
73
+
74
+ uri = URI.parse( attachment.url )
75
+ begin
76
+ ENV["PGPASSWORD"] = uri.password
77
+ ENV["PGSSLMODE"] = 'require'
78
+ exec "psql -U #{uri.user} -h #{uri.host} -p #{uri.port || 5432} #{uri.path[1..-1]}"
79
+ rescue Errno::ENOENT
80
+ output_with_bang "The local psql command could not be located"
81
+ output_with_bang "For help installing psql, see http://devcenter.azukiapp.com/articles/local-postgresql"
82
+ abort
83
+ end
84
+ end
85
+
86
+ # pg:reset DATABASE
87
+ #
88
+ # Delete all data in DATABASE
89
+ #
90
+ def reset
91
+ unless db = shift_argument
92
+ error("Usage: azuki pg:reset DATABASE\nMust specify DATABASE to reset.")
93
+ end
94
+ validate_arguments!
95
+
96
+ attachment = hpg_resolve(db) unless db == "SHARED_DATABASE"
97
+ return unless confirm_command
98
+
99
+ if db == "SHARED_DATABASE"
100
+ action("Resetting SHARED_DATABASE") { azuki.database_reset(app) }
101
+ else
102
+ action("Resetting #{attachment.display_name}") do
103
+ hpg_client(attachment).reset
104
+ end
105
+ end
106
+ end
107
+
108
+ # pg:unfollow REPLICA
109
+ #
110
+ # stop a replica from following and make it a read/write database
111
+ #
112
+ def unfollow
113
+ unless db = shift_argument
114
+ error("Usage: azuki pg:unfollow REPLICA\nMust specify REPLICA to unfollow.")
115
+ end
116
+ validate_arguments!
117
+
118
+ replica = hpg_resolve(db)
119
+ replica_info = hpg_info(replica)
120
+
121
+ unless replica_info[:following]
122
+ error("#{replica.display_name} is not following another database.")
123
+ end
124
+ origin_url = replica_info[:following]
125
+ origin_name = database_name_from_url(origin_url)
126
+
127
+ output_with_bang "#{replica.display_name} will become writable and no longer"
128
+ output_with_bang "follow #{origin_name}. This cannot be undone."
129
+ return unless confirm_command
130
+
131
+ action "Unfollowing #{replica.display_name}" do
132
+ hpg_client(replica).unfollow
133
+ end
134
+ end
135
+
136
+ # pg:wait [DATABASE]
137
+ #
138
+ # monitor database creation, exit when complete
139
+ #
140
+ # defaults to all databases if no DATABASE is specified
141
+ #
142
+ def wait
143
+ db = shift_argument
144
+ validate_arguments!
145
+
146
+ if db
147
+ wait_for hpg_resolve(db)
148
+ else
149
+ hpg_databases.values.each do |attach|
150
+ wait_for(attach)
151
+ end
152
+ end
153
+ end
154
+
155
+ # pg:credentials DATABASE
156
+ #
157
+ # Display the DATABASE credentials.
158
+ #
159
+ # --reset # Reset credentials on the specified database.
160
+ #
161
+ def credentials
162
+ unless db = shift_argument
163
+ error("Usage: azuki pg:credentials DATABASE\nMust specify DATABASE to display credentials.")
164
+ end
165
+ validate_arguments!
166
+
167
+ attachment = hpg_resolve(db)
168
+
169
+ if options[:reset]
170
+ action "Resetting credentials for #{attachment.display_name}" do
171
+ hpg_client(attachment).rotate_credentials
172
+ end
173
+ if attachment.primary_attachment?
174
+ forget_config!
175
+ attachment = hpg_resolve(db)
176
+ action "Promoting #{attachment.display_name}" do
177
+ hpg_promote(attachment.url)
178
+ end
179
+ end
180
+ else
181
+ uri = URI.parse( attachment.url )
182
+ display "Connection info string:"
183
+ display " \"dbname=#{uri.path[1..-1]} host=#{uri.host} port=#{uri.port || 5432} user=#{uri.user} password=#{uri.password} sslmode=require\""
184
+ display "Connection URL:"
185
+ display " " + attachment.url
186
+
187
+ end
188
+ end
189
+
190
+ private
191
+
192
+ def database_name_from_url(url)
193
+ vars = app_config_vars.reject {|key,value| key == 'DATABASE_URL'}
194
+ if var = vars.invert[url]
195
+ var.gsub(/_URL$/, '')
196
+ else
197
+ uri = URI.parse(url)
198
+ "Database on #{uri.host}:#{uri.port || 5432}#{uri.path}"
199
+ end
200
+ end
201
+
202
+ def display_db(name, db)
203
+ styled_header(name)
204
+ styled_hash(db[:info].inject({}) do |hash, item|
205
+ hash.update(item["name"] => hpg_info_display(item))
206
+ end, db[:info].map {|item| item['name']})
207
+
208
+ display
209
+ end
210
+
211
+ def hpg_client(attachment)
212
+ Azuki::Client::AzukiPostgresql.new(attachment)
213
+ end
214
+
215
+ def hpg_databases_with_info
216
+ return @hpg_databases_with_info if @hpg_databases_with_info
217
+
218
+ @hpg_databases_with_info = Hash[ hpg_databases.map { |config, att| [att.display_name, hpg_info(att, options[:extended])] } ]
219
+
220
+ return @hpg_databases_with_info
221
+ end
222
+
223
+ def hpg_info(attachment, extended=false)
224
+ if attachment.resource_name == "SHARED_DATABASE"
225
+ data = api.get_app(app).body
226
+ {:info => [{
227
+ 'name' => 'Data Size',
228
+ 'values' => [format_bytes(data['database_size'])]
229
+ }]}
230
+ else
231
+ hpg_client(attachment).get_database(extended)
232
+ end
233
+ end
234
+
235
+ def hpg_info_display(item)
236
+ item["values"] = [item["value"]] if item["value"]
237
+ item["values"].map do |value|
238
+ if item["resolve_db_name"]
239
+ database_name_from_url(value)
240
+ else
241
+ value
242
+ end
243
+ end
244
+ end
245
+
246
+ def ticking
247
+ ticks = 0
248
+ loop do
249
+ yield(ticks)
250
+ ticks +=1
251
+ sleep 1
252
+ end
253
+ end
254
+
255
+ def wait_for(attach)
256
+ ticking do |ticks|
257
+ status = hpg_client(attach).get_wait_status
258
+ error status[:message] if status[:error?]
259
+ break if !status[:waiting?] && ticks.zero?
260
+ redisplay("Waiting for database %s... %s%s" % [
261
+ attach.display_name,
262
+ status[:waiting?] ? "#{spinner(ticks)} " : "",
263
+ status[:message]],
264
+ !status[:waiting?]) # only display a newline on the last tick
265
+ break unless status[:waiting?]
266
+ end
267
+ end
268
+
269
+ end