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,86 @@
1
+ require "azuki/command/base"
2
+
3
+ # authentication (login, logout)
4
+ #
5
+ class Azuki::Command::Auth < Azuki::Command::Base
6
+
7
+ # auth
8
+ #
9
+ # Authenticate, display token and current user
10
+ def index
11
+ validate_arguments!
12
+
13
+ Azuki::Command::Help.new.send(:help_for_command, current_command)
14
+ end
15
+
16
+ # auth:login
17
+ #
18
+ # log in with your azuki credentials
19
+ #
20
+ #Example:
21
+ #
22
+ # $ azuki auth:login
23
+ # Enter your Azuki credentials:
24
+ # Email: email@example.com
25
+ # Password (typing will be hidden):
26
+ # Authentication successful.
27
+ #
28
+ def login
29
+ validate_arguments!
30
+
31
+ Azuki::Auth.login
32
+ display "Authentication successful."
33
+ end
34
+
35
+ alias_command "login", "auth:login"
36
+
37
+ # auth:logout
38
+ #
39
+ # clear local authentication credentials
40
+ #
41
+ #Example:
42
+ #
43
+ # $ azuki auth:logout
44
+ # Local credentials cleared.
45
+ #
46
+ def logout
47
+ validate_arguments!
48
+
49
+ Azuki::Auth.logout
50
+ display "Local credentials cleared."
51
+ end
52
+
53
+ alias_command "logout", "auth:logout"
54
+
55
+ # auth:token
56
+ #
57
+ # display your api token
58
+ #
59
+ #Example:
60
+ #
61
+ # $ azuki auth:token
62
+ # ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCD
63
+ #
64
+ def token
65
+ validate_arguments!
66
+
67
+ display Azuki::Auth.api_key
68
+ end
69
+
70
+ # auth:whoami
71
+ #
72
+ # display your azuki email address
73
+ #
74
+ #Example:
75
+ #
76
+ # $ azuki auth:whoami
77
+ # email@example.com
78
+ #
79
+ def whoami
80
+ validate_arguments!
81
+
82
+ display Azuki::Auth.user
83
+ end
84
+
85
+ end
86
+
@@ -0,0 +1,230 @@
1
+ require "fileutils"
2
+ require "azuki/auth"
3
+ require "azuki/client/rendezvous"
4
+ require "azuki/command"
5
+
6
+ class Azuki::Command::Base
7
+ include Azuki::Helpers
8
+
9
+ def self.namespace
10
+ self.to_s.split("::").last.downcase
11
+ end
12
+
13
+ attr_reader :args
14
+ attr_reader :options
15
+
16
+ def initialize(args=[], options={})
17
+ @args = args
18
+ @options = options
19
+ end
20
+
21
+ def app
22
+ @app ||= if options[:confirm].is_a?(String)
23
+ if options[:app] && (options[:app] != options[:confirm])
24
+ error("Mismatch between --app and --confirm")
25
+ end
26
+ options[:confirm]
27
+ elsif options[:app].is_a?(String)
28
+ options[:app]
29
+ elsif ENV.has_key?('AZUKI_APP')
30
+ ENV['AZUKI_APP']
31
+ elsif app_from_dir = extract_app_in_dir(Dir.pwd)
32
+ app_from_dir
33
+ else
34
+ # raise instead of using error command to enable rescuing when app is optional
35
+ raise Azuki::Command::CommandFailed.new("No app specified.\nRun this command from an app folder or specify which app to use with --app APP.")
36
+ end
37
+ end
38
+
39
+ def api
40
+ Azuki::Auth.api
41
+ end
42
+
43
+ def azuki
44
+ Azuki::Auth.client
45
+ end
46
+
47
+ protected
48
+
49
+ def self.inherited(klass)
50
+ unless klass == Azuki::Command::Base
51
+ help = extract_help_from_caller(caller.first)
52
+
53
+ Azuki::Command.register_namespace(
54
+ :name => klass.namespace,
55
+ :description => help.first
56
+ )
57
+ end
58
+ end
59
+
60
+ def self.method_added(method)
61
+ return if self == Azuki::Command::Base
62
+ return if private_method_defined?(method)
63
+ return if protected_method_defined?(method)
64
+
65
+ help = extract_help_from_caller(caller.first)
66
+ resolved_method = (method.to_s == "index") ? nil : method.to_s
67
+ command = [ self.namespace, resolved_method ].compact.join(":")
68
+ banner = extract_banner(help) || command
69
+
70
+ Azuki::Command.register_command(
71
+ :klass => self,
72
+ :method => method,
73
+ :namespace => self.namespace,
74
+ :command => command,
75
+ :banner => banner.strip,
76
+ :help => help.join("\n"),
77
+ :summary => extract_summary(help),
78
+ :description => extract_description(help),
79
+ :options => extract_options(help)
80
+ )
81
+ end
82
+
83
+ def self.alias_command(new, old)
84
+ raise "no such command: #{old}" unless Azuki::Command.commands[old]
85
+ Azuki::Command.command_aliases[new] = old
86
+ end
87
+
88
+ def extract_app
89
+ output_with_bang "Command::Base#extract_app has been deprecated. Please use Command::Base#app instead. #{caller.first}"
90
+ app
91
+ end
92
+
93
+ #
94
+ # Parse the caller format and identify the file and line number as identified
95
+ # in : http://www.ruby-doc.org/core/classes/Kernel.html#M001397. This will
96
+ # look for a colon followed by a digit as the delimiter. The biggest
97
+ # complication is windows paths, which have a color after the drive letter.
98
+ # This regex will match paths as anything from the beginning to a colon
99
+ # directly followed by a number (the line number).
100
+ #
101
+ # Examples of the caller format :
102
+ # * c:/Ruby192/lib/.../lib/azuki/command/addons.rb:8:in `<module:Command>'
103
+ # * c:/Ruby192/lib/.../azuki-2.0.1/lib/azuki/command/pg.rb:96:in `<class:Pg>'
104
+ # * /Users/ph7/...../xray-1.1/lib/xray/thread_dump_signal_handler.rb:9
105
+ #
106
+ def self.extract_help_from_caller(line)
107
+ # pull out of the caller the information for the file path and line number
108
+ if line =~ /^(.+?):(\d+)/
109
+ extract_help($1, $2)
110
+ else
111
+ raise("unable to extract help from caller: #{line}")
112
+ end
113
+ end
114
+
115
+ def self.extract_help(file, line_number)
116
+ buffer = []
117
+ lines = Azuki::Command.files[file]
118
+
119
+ (line_number.to_i-2).downto(0) do |i|
120
+ line = lines[i]
121
+ case line[0..0]
122
+ when ""
123
+ when "#"
124
+ buffer.unshift(line[1..-1])
125
+ else
126
+ break
127
+ end
128
+ end
129
+
130
+ buffer
131
+ end
132
+
133
+ def self.extract_banner(help)
134
+ help.first
135
+ end
136
+
137
+ def self.extract_summary(help)
138
+ extract_description(help).split("\n")[2].to_s.split("\n").first
139
+ end
140
+
141
+ def self.extract_description(help)
142
+ help.reject do |line|
143
+ line =~ /^\s+-(.+)#(.+)/
144
+ end.join("\n")
145
+ end
146
+
147
+ def self.extract_options(help)
148
+ help.select do |line|
149
+ line =~ /^\s+-(.+)#(.+)/
150
+ end.inject([]) do |options, line|
151
+ args = line.split('#', 2).first
152
+ args = args.split(/,\s*/).map {|arg| arg.strip}.sort.reverse
153
+ name = args.last.split(' ', 2).first[2..-1]
154
+ options << { :name => name, :args => args }
155
+ end
156
+ end
157
+
158
+ def current_command
159
+ Azuki::Command.current_command
160
+ end
161
+
162
+ def extract_option(key)
163
+ options[key.dup.gsub('-','_').to_sym]
164
+ end
165
+
166
+ def invalid_arguments
167
+ Azuki::Command.invalid_arguments
168
+ end
169
+
170
+ def shift_argument
171
+ Azuki::Command.shift_argument
172
+ end
173
+
174
+ def validate_arguments!
175
+ Azuki::Command.validate_arguments!
176
+ end
177
+
178
+ def extract_app_in_dir(dir)
179
+ return unless remotes = git_remotes(dir)
180
+
181
+ if remote = options[:remote]
182
+ remotes[remote]
183
+ elsif remote = extract_app_from_git_config
184
+ remotes[remote]
185
+ else
186
+ apps = remotes.values.uniq
187
+ if apps.size == 1
188
+ apps.first
189
+ else
190
+ raise(Azuki::Command::CommandFailed, "Multiple apps in folder and no app specified.\nSpecify app with --app APP.")
191
+ end
192
+ end
193
+ end
194
+
195
+ def extract_app_from_git_config
196
+ remote = git("config azuki.remote")
197
+ remote == "" ? nil : remote
198
+ end
199
+
200
+ def git_remotes(base_dir=Dir.pwd)
201
+ remotes = {}
202
+ original_dir = Dir.pwd
203
+ Dir.chdir(base_dir)
204
+
205
+ return unless File.exists?(".git")
206
+ git("remote -v").split("\n").each do |remote|
207
+ name, url, method = remote.split(/\s/)
208
+ if url =~ /^git@#{Azuki::Auth.git_host}(?:[\.\w]*):([\w\d-]+)\.git$/
209
+ remotes[name] = $1
210
+ end
211
+ end
212
+
213
+ Dir.chdir(original_dir)
214
+ if remotes.empty?
215
+ nil
216
+ else
217
+ remotes
218
+ end
219
+ end
220
+
221
+ def escape(value)
222
+ azuki.escape(value)
223
+ end
224
+ end
225
+
226
+ module Azuki::Command
227
+ unless const_defined?(:BaseWithApp)
228
+ BaseWithApp = Base
229
+ end
230
+ end
@@ -0,0 +1,209 @@
1
+ require "azuki/command/base"
2
+ require "excon"
3
+
4
+ # manage ssl endpoints for an app
5
+ #
6
+ class Azuki::Command::Certs < Azuki::Command::Base
7
+ SSL_DOCTOR = Excon.new(ENV["SSL_DOCTOR_URL"] || "https://ssl-doctor.azukiapp.com/")
8
+
9
+ class UsageError < StandardError; end
10
+
11
+ # certs
12
+ #
13
+ # List ssl endpoints for an app.
14
+ #
15
+ def index
16
+ endpoints = azuki.ssl_endpoint_list(app)
17
+
18
+ if endpoints.empty?
19
+ display "#{app} has no SSL Endpoints."
20
+ display "Use `azuki certs:add CRT KEY` to add one."
21
+ else
22
+ endpoints.map! do |endpoint|
23
+ {
24
+ 'cname' => endpoint['cname'],
25
+ 'domains' => endpoint['ssl_cert']['cert_domains'].join(', '),
26
+ 'expires_at' => format_date(endpoint['ssl_cert']['expires_at']),
27
+ 'ca_signed?' => endpoint['ssl_cert']['ca_signed?'].to_s.capitalize
28
+ }
29
+ end
30
+ display_table(
31
+ endpoints,
32
+ %w( cname domains expires_at ca_signed? ),
33
+ [ "Endpoint", "Common Name(s)", "Expires", "Trusted" ]
34
+ )
35
+ end
36
+ end
37
+
38
+ # certs:chain CRT [CRT ...]
39
+ #
40
+ # Print the ordered and complete chain for the given certificate.
41
+ #
42
+ # Optional intermediate certificates may be given too, and will
43
+ # be used during chain resolution.
44
+ #
45
+ def chain
46
+ puts read_crt_through_ssl_doctor
47
+ rescue UsageError
48
+ fail("Usage: azuki certs:chain CRT [CRT ...]\nMust specify at least one certificate file.")
49
+ end
50
+
51
+ # certs:key CRT KEY [KEY ...]
52
+ #
53
+ # Print the correct key for the given certificate.
54
+ #
55
+ # You must pass one single certificate, and one or more keys.
56
+ # The first key that signs the certificate will be printed back.
57
+ #
58
+ def key
59
+ crt, key = read_crt_and_key_through_ssl_doctor("Testing for signing key")
60
+ puts key
61
+ rescue UsageError
62
+ fail("Usage: azuki certs:key CRT KEY [KEY ...]\nMust specify one certificate file and at least one key file.")
63
+ end
64
+
65
+ # certs:add CRT KEY
66
+ #
67
+ # Add an ssl endpoint to an app.
68
+ #
69
+ # --bypass # bypass the trust chain completion step
70
+ #
71
+ def add
72
+ crt, key = read_crt_and_key
73
+ endpoint = action("Adding SSL Endpoint to #{app}") { azuki.ssl_endpoint_add(app, crt, key) }
74
+ display_warnings(endpoint)
75
+ display "#{app} now served by #{endpoint['cname']}"
76
+ display "Certificate details:"
77
+ display_certificate_info(endpoint)
78
+ rescue UsageError
79
+ fail("Usage: azuki certs:add CRT KEY\nMust specify CRT and KEY to add cert.")
80
+ end
81
+
82
+ # certs:update CRT KEY
83
+ #
84
+ # Update an SSL Endpoint on an app.
85
+ #
86
+ # --bypass # bypass the trust chain completion step
87
+ #
88
+ def update
89
+ crt, key = read_crt_and_key
90
+ cname = options[:endpoint] || current_endpoint
91
+ endpoint = action("Updating SSL Endpoint #{cname} for #{app}") { azuki.ssl_endpoint_update(app, cname, crt, key) }
92
+ display_warnings(endpoint)
93
+ display "Updated certificate details:"
94
+ display_certificate_info(endpoint)
95
+ rescue UsageError
96
+ fail("Usage: azuki certs:update CRT KEY\nMust specify CRT and KEY to update cert.")
97
+ end
98
+
99
+ # certs:info
100
+ #
101
+ # Show certificate information for an ssl endpoint.
102
+ #
103
+ def info
104
+ cname = options[:endpoint] || current_endpoint
105
+ endpoint = action("Fetching SSL Endpoint #{cname} info for #{app}") do
106
+ azuki.ssl_endpoint_info(app, cname)
107
+ end
108
+
109
+ display "Certificate details:"
110
+ display_certificate_info(endpoint)
111
+ end
112
+
113
+ # certs:remove
114
+ #
115
+ # Remove an SSL Endpoint from an app.
116
+ #
117
+ def remove
118
+ cname = options[:endpoint] || current_endpoint
119
+ action("Removing SSL Endpoint #{cname} from #{app}") do
120
+ azuki.ssl_endpoint_remove(app, cname)
121
+ end
122
+ display "NOTE: Billing is still active. Remove SSL Endpoint add-on to stop billing."
123
+ end
124
+
125
+ # certs:rollback
126
+ #
127
+ # Rollback an SSL Endpoint for an app.
128
+ #
129
+ def rollback
130
+ cname = options[:endpoint] || current_endpoint
131
+
132
+ endpoint = action("Rolling back SSL Endpoint #{cname} for #{app}") do
133
+ azuki.ssl_endpoint_rollback(app, cname)
134
+ end
135
+
136
+ display "New active certificate details:"
137
+ display_certificate_info(endpoint)
138
+ end
139
+
140
+ private
141
+
142
+ def current_endpoint
143
+ endpoint = azuki.ssl_endpoint_list(app).first || error("#{app} has no SSL Endpoints.")
144
+ endpoint["cname"]
145
+ end
146
+
147
+ def display_certificate_info(endpoint)
148
+ data = {
149
+ 'Common Name(s)' => endpoint['ssl_cert']['cert_domains'],
150
+ 'Expires At' => format_date(endpoint['ssl_cert']['expires_at']),
151
+ 'Issuer' => endpoint['ssl_cert']['issuer'],
152
+ 'Starts At' => format_date(endpoint['ssl_cert']['starts_at']),
153
+ 'Subject' => endpoint['ssl_cert']['subject']
154
+ }
155
+ styled_hash(data)
156
+
157
+ if endpoint["ssl_cert"]["ca_signed?"]
158
+ display "SSL certificate is verified by a root authority."
159
+ elsif endpoint["issuer"] == endpoint["subject"]
160
+ display "SSL certificate is self signed."
161
+ else
162
+ display "SSL certificate is not trusted."
163
+ end
164
+ end
165
+
166
+ def display_warnings(endpoint)
167
+ if endpoint["warnings"]
168
+ endpoint["warnings"].each do |field, warning|
169
+ display "WARNING: #{field} #{warning}"
170
+ end
171
+ end
172
+ end
173
+
174
+ def display(msg = "", new_line = true)
175
+ super if $stdout.tty?
176
+ end
177
+
178
+ def post_to_ssl_doctor(path, action_text = nil)
179
+ raise UsageError if args.size < 1
180
+ action_text ||= "Resolving trust chain"
181
+ action(action_text) do
182
+ input = args.map { |arg| File.read(arg) rescue error("Unable to read #{args[0]} file") }.join("\n")
183
+ SSL_DOCTOR.post(:path => path, :body => input, :headers => {'Content-Type' => 'application/octet-stream'}, :expects => 200).body
184
+ end
185
+ rescue Excon::Errors::BadRequest, Excon::Errors::UnprocessableEntity => e
186
+ error(e.response.body)
187
+ end
188
+
189
+ def read_crt_and_key_through_ssl_doctor(action_text = nil)
190
+ crt_and_key = post_to_ssl_doctor("resolve-chain-and-key", action_text)
191
+ Azuki::OkJson.decode(crt_and_key).values_at("pem", "key")
192
+ end
193
+
194
+ def read_crt_through_ssl_doctor(action_text = nil)
195
+ post_to_ssl_doctor("resolve-chain", action_text)
196
+ end
197
+
198
+ def read_crt_and_key_bypassing_ssl_doctor
199
+ raise UsageError if args.size != 2
200
+ crt = File.read(args[0]) rescue error("Unable to read #{args[0]} CRT")
201
+ key = File.read(args[1]) rescue error("Unable to read #{args[1]} KEY")
202
+ [crt, key]
203
+ end
204
+
205
+ def read_crt_and_key
206
+ options[:bypass] ? read_crt_and_key_bypassing_ssl_doctor : read_crt_and_key_through_ssl_doctor
207
+ end
208
+
209
+ end