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