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