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,141 @@
1
+ require "azuki/client"
2
+
3
+ class Azuki::Client::AzukiPostgresql
4
+ Version = 11
5
+
6
+ include Azuki::Helpers
7
+
8
+ @headers = { :x_azuki_gem_version => Azuki::Client.version }
9
+
10
+ def self.add_headers(headers)
11
+ @headers.merge! headers
12
+ end
13
+
14
+ def self.headers
15
+ @headers
16
+ end
17
+
18
+ attr_reader :attachment
19
+ def initialize(attachment)
20
+ @attachment = attachment
21
+ if attachment.resource_name == 'SHARED_DATABASE'
22
+ error('This command is not available for shared database')
23
+ end
24
+ require 'rest_client'
25
+ end
26
+
27
+ def azuki_postgresql_host
28
+ if attachment.starter_plan?
29
+ ENV["AZUKI_POSTGRESQL_HOST"] || "postgres-starter-api"
30
+ else
31
+ if ENV['SHOGUN']
32
+ "shogun-#{ENV['SHOGUN']}"
33
+ else
34
+ ENV["AZUKI_POSTGRESQL_HOST"] || "postgres-api"
35
+ end
36
+ end
37
+ end
38
+
39
+ def resource_name
40
+ attachment.resource_name
41
+ end
42
+
43
+ def azuki_postgresql_resource
44
+ RestClient::Resource.new(
45
+ "https://#{azuki_postgresql_host}.azukiapp.com/client/v11/databases",
46
+ :user => Azuki::Auth.user,
47
+ :password => Azuki::Auth.password,
48
+ :headers => self.class.headers
49
+ )
50
+ end
51
+
52
+ def ingress
53
+ http_put "#{resource_name}/ingress"
54
+ end
55
+
56
+ def reset
57
+ http_put "#{resource_name}/reset"
58
+ end
59
+
60
+ def rotate_credentials
61
+ http_post "#{resource_name}/credentials_rotation"
62
+ end
63
+
64
+ def get_database(extended=false)
65
+ query = extended ? '?extended=true' : ''
66
+ http_get resource_name + query
67
+ end
68
+
69
+ def get_wait_status
70
+ http_get "#{resource_name}/wait_status"
71
+ end
72
+
73
+ def unfollow
74
+ http_put "#{resource_name}/unfollow"
75
+ end
76
+
77
+ protected
78
+
79
+ def sym_keys(c)
80
+ if c.is_a?(Array)
81
+ c.map { |e| sym_keys(e) }
82
+ else
83
+ c.inject({}) do |h, (k, v)|
84
+ h[k.to_sym] = v; h
85
+ end
86
+ end
87
+ end
88
+
89
+ def checking_client_version
90
+ begin
91
+ yield
92
+ rescue RestClient::BadRequest => e
93
+ if message = json_decode(e.response.to_s)["upgrade_message"]
94
+ abort(message)
95
+ else
96
+ raise e
97
+ end
98
+ end
99
+ end
100
+
101
+ def display_azuki_warning(response)
102
+ warning = response.headers[:x_azuki_warning]
103
+ display warning if warning
104
+ response
105
+ end
106
+
107
+ def http_get(path)
108
+ checking_client_version do
109
+ retry_on_exception(RestClient::Exception) do
110
+ response = azuki_postgresql_resource[path].get
111
+ display_azuki_warning response
112
+ sym_keys(json_decode(response.to_s))
113
+ end
114
+ end
115
+ end
116
+
117
+ def http_post(path, payload = {})
118
+ checking_client_version do
119
+ response = azuki_postgresql_resource[path].post(json_encode(payload))
120
+ display_azuki_warning response
121
+ sym_keys(json_decode(response.to_s))
122
+ end
123
+ end
124
+
125
+ def http_put(path, payload = {})
126
+ checking_client_version do
127
+ response = azuki_postgresql_resource[path].put(json_encode(payload))
128
+ display_azuki_warning response
129
+ sym_keys(json_decode(response.to_s))
130
+ end
131
+ end
132
+ end
133
+
134
+ module AzukiPostgresql
135
+ class Client < Azuki::Client::AzukiPostgresql
136
+ def initialize(*args)
137
+ Azuki::Helpers.deprecate "AzukiPostgresql::Client has been deprecated. Please use Azuki::Client::AzukiPostgresql instead."
138
+ super
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,26 @@
1
+ require "azuki/client"
2
+
3
+ class Azuki::Client::Cisaurus
4
+
5
+ include Azuki::Helpers
6
+
7
+ def initialize(uri)
8
+ require 'rest_client'
9
+ @uri = URI.parse(uri)
10
+ end
11
+
12
+ def authenticated_resource(path)
13
+ host = "#{@uri.scheme}://#{@uri.host}"
14
+ host += ":#{@uri.port}" if @uri.port
15
+ RestClient::Resource.new("#{host}#{path}", "", Azuki::Auth.api_key)
16
+ end
17
+
18
+ def copy_slug(from, to)
19
+ authenticated_resource("/v1/apps/#{from}/copy/#{to}").post(json_encode("description" => "Forked from #{from}"), :content_type => :json).headers[:location]
20
+ end
21
+
22
+ def job_done?(job_location)
23
+ done = authenticated_resource(job_location).get.code
24
+ done == 202
25
+ end
26
+ end
@@ -0,0 +1,113 @@
1
+ require "azuki/client"
2
+
3
+ class Azuki::Client::Pgbackups
4
+
5
+ include Azuki::Helpers
6
+
7
+ def initialize(uri)
8
+ require 'rest_client'
9
+ @uri = URI.parse(uri)
10
+ end
11
+
12
+ def authenticated_resource(path)
13
+ host = "#{@uri.scheme}://#{@uri.host}"
14
+ host += ":#{@uri.port}" if @uri.port
15
+ RestClient::Resource.new("#{host}#{path}",
16
+ :user => @uri.user,
17
+ :password => @uri.password,
18
+ :headers => {:x_azuki_gem_version => Azuki::Client.version}
19
+ )
20
+ end
21
+
22
+ def create_transfer(from_url, from_name, to_url, to_name, opts={})
23
+ # opts[:expire] => true will delete the oldest backup if at the plan limit
24
+ resource = authenticated_resource("/client/transfers")
25
+ params = {:from_url => from_url, :from_name => from_name, :to_url => to_url, :to_name => to_name}.merge opts
26
+ json_decode post(resource, params).body
27
+ end
28
+
29
+ def get_transfers
30
+ resource = authenticated_resource("/client/transfers")
31
+ json_decode get(resource).body
32
+ end
33
+
34
+ def get_transfer(id)
35
+ resource = authenticated_resource("/client/transfers/#{id}")
36
+ json_decode get(resource).body
37
+ end
38
+
39
+ def get_backups(opts={})
40
+ resource = authenticated_resource("/client/backups")
41
+ json_decode get(resource).body
42
+ end
43
+
44
+ def get_backup(name, opts={})
45
+ name = URI.escape(name)
46
+ resource = authenticated_resource("/client/backups/#{name}")
47
+ json_decode get(resource).body
48
+ end
49
+
50
+ def get_latest_backup
51
+ resource = authenticated_resource("/client/latest_backup")
52
+ json_decode get(resource).body
53
+ end
54
+
55
+ def delete_backup(name)
56
+ name = URI.escape(name)
57
+ begin
58
+ resource = authenticated_resource("/client/backups/#{name}")
59
+ delete(resource).body
60
+ true
61
+ rescue RestClient::ResourceNotFound => e
62
+ false
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def get(resource)
69
+ check_errors do
70
+ response = resource.get
71
+ display_azuki_warning response
72
+ response
73
+ end
74
+ end
75
+
76
+ def post(resource, params)
77
+ check_errors do
78
+ response = resource.post(params)
79
+ display_azuki_warning response
80
+ response
81
+ end
82
+ end
83
+
84
+ def delete(resource)
85
+ check_errors do
86
+ response = resource.delete
87
+ display_azuki_warning response
88
+ response
89
+ end
90
+ end
91
+
92
+ def check_errors
93
+ yield
94
+ rescue RestClient::Unauthorized
95
+ error "Invalid PGBACKUPS_URL"
96
+ end
97
+
98
+ def display_azuki_warning(response)
99
+ warning = response.headers[:x_azuki_warning]
100
+ display warning if warning
101
+ response
102
+ end
103
+
104
+ end
105
+
106
+ module Pgbackups
107
+ class Client < Azuki::Client::Pgbackups
108
+ def initialize(*args)
109
+ Azuki::Helpers.deprecate "Pgbackups::Client has been deprecated. Please use Azuki::Client::Pgbackups instead."
110
+ super
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,108 @@
1
+ require "timeout"
2
+ require "socket"
3
+ require "uri"
4
+ require "azuki/auth"
5
+ require "azuki/client"
6
+ require "azuki/helpers"
7
+
8
+ class Azuki::Client::Rendezvous
9
+
10
+ include Azuki::Helpers
11
+
12
+ attr_reader :rendezvous_url, :connect_timeout, :activity_timeout, :input, :output, :on_connect
13
+
14
+ def initialize(opts)
15
+ @rendezvous_url = opts[:rendezvous_url]
16
+ @connect_timeout = opts[:connect_timeout]
17
+ @activity_timeout = opts[:activity_timeout]
18
+ @input = opts[:input]
19
+ @output = opts[:output]
20
+ end
21
+
22
+ def on_connect(&blk)
23
+ @on_connect = blk if block_given?
24
+ @on_connect
25
+ end
26
+
27
+ def start
28
+ uri = URI.parse(rendezvous_url)
29
+ host, port, secret = uri.host, uri.port, uri.path[1..-1]
30
+
31
+ ssl_socket = Timeout.timeout(connect_timeout) do
32
+ ssl_context = OpenSSL::SSL::SSLContext.new
33
+
34
+ if Azuki::Auth.verify_host?(host)
35
+ ssl_context.ca_file = File.expand_path("../../../../data/cacert.pem", __FILE__)
36
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
37
+ end
38
+
39
+ tcp_socket = TCPSocket.open(host, port)
40
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
41
+ ssl_socket.connect
42
+ ssl_socket.puts(secret)
43
+ ssl_socket.readline
44
+ ssl_socket
45
+ end
46
+
47
+ on_connect.call if on_connect
48
+
49
+ readables = [input, ssl_socket].compact
50
+
51
+ begin
52
+ loop do
53
+ if o = IO.select(readables, nil, nil, activity_timeout)
54
+ if (input && (o.first.first == input))
55
+ begin
56
+ data = input.readpartial(10000)
57
+ rescue EOFError
58
+ readables.delete(input)
59
+ next
60
+ end
61
+ if running_on_windows?
62
+ data.gsub!("\r\n", "\n") # prevent double CRs
63
+ end
64
+ ssl_socket.write(data)
65
+ ssl_socket.flush
66
+ elsif (o.first.first == ssl_socket)
67
+ begin
68
+ data = ssl_socket.readpartial(10000)
69
+ rescue EOFError
70
+ break
71
+ end
72
+ output.write(fixup(data))
73
+ end
74
+ else
75
+ raise(Timeout::Error.new)
76
+ end
77
+ end
78
+ rescue Interrupt
79
+ ssl_socket.write(3.chr)
80
+ ssl_socket.flush
81
+ retry
82
+ rescue SignalException => e
83
+ if Signal.list["QUIT"] == e.signo
84
+ ssl_socket.write(28.chr)
85
+ ssl_socket.flush
86
+ retry
87
+ end
88
+ raise
89
+ rescue Errno::EIO
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def fixup(data)
96
+ return nil if ! data
97
+ if data.respond_to?(:force_encoding)
98
+ data.force_encoding('utf-8') if data.respond_to?(:force_encoding)
99
+ end
100
+ if running_on_windows?
101
+ begin
102
+ data.gsub!(/\e\[[\d;]+m/, '')
103
+ rescue # ignore failed gsub, for instance when non-utf8
104
+ end
105
+ end
106
+ output.isatty ? data : data.gsub(/\cM/,"")
107
+ end
108
+ end
@@ -0,0 +1,25 @@
1
+ class Azuki::Client
2
+ def ssl_endpoint_add(app, pem, key)
3
+ json_decode(post("apps/#{app}/ssl-endpoints", :accept => :json, :pem => pem, :key => key).to_s)
4
+ end
5
+
6
+ def ssl_endpoint_info(app, cname)
7
+ json_decode(get("apps/#{app}/ssl-endpoints/#{escape(cname)}", :accept => :json).to_s)
8
+ end
9
+
10
+ def ssl_endpoint_list(app)
11
+ json_decode(get("apps/#{app}/ssl-endpoints", :accept => :json).to_s)
12
+ end
13
+
14
+ def ssl_endpoint_remove(app, cname)
15
+ json_decode(delete("apps/#{app}/ssl-endpoints/#{escape(cname)}", :accept => :json).to_s)
16
+ end
17
+
18
+ def ssl_endpoint_rollback(app, cname)
19
+ json_decode(post("apps/#{app}/ssl-endpoints/#{escape(cname)}/rollback", :accept => :json).to_s)
20
+ end
21
+
22
+ def ssl_endpoint_update(app, cname, pem, key)
23
+ json_decode(put("apps/#{app}/ssl-endpoints/#{escape(cname)}", :accept => :json, :pem => pem, :key => key).to_s)
24
+ end
25
+ end
@@ -0,0 +1,294 @@
1
+ require 'azuki/helpers'
2
+ require 'azuki/plugin'
3
+ require 'azuki/version'
4
+ require "optparse"
5
+
6
+ module Azuki
7
+ module Command
8
+ class CommandFailed < RuntimeError; end
9
+
10
+ extend Azuki::Helpers
11
+
12
+ def self.load
13
+ Dir[File.join(File.dirname(__FILE__), "command", "*.rb")].each do |file|
14
+ require file
15
+ end
16
+ Azuki::Plugin.load!
17
+ end
18
+
19
+ def self.commands
20
+ @@commands ||= {}
21
+ end
22
+
23
+ def self.command_aliases
24
+ @@command_aliases ||= {}
25
+ end
26
+
27
+ def self.files
28
+ @@files ||= Hash.new {|hash,key| hash[key] = File.readlines(key).map {|line| line.strip}}
29
+ end
30
+
31
+ def self.namespaces
32
+ @@namespaces ||= {}
33
+ end
34
+
35
+ def self.register_command(command)
36
+ commands[command[:command]] = command
37
+ end
38
+
39
+ def self.register_namespace(namespace)
40
+ namespaces[namespace[:name]] = namespace
41
+ end
42
+
43
+ def self.current_command
44
+ @current_command
45
+ end
46
+
47
+ def self.current_command=(new_current_command)
48
+ @current_command = new_current_command
49
+ end
50
+
51
+ def self.current_args
52
+ @current_args
53
+ end
54
+
55
+ def self.current_options
56
+ @current_options ||= {}
57
+ end
58
+
59
+ def self.global_options
60
+ @global_options ||= []
61
+ end
62
+
63
+ def self.invalid_arguments
64
+ @invalid_arguments
65
+ end
66
+
67
+ def self.shift_argument
68
+ # dup argument to get a non-frozen string
69
+ @invalid_arguments.shift.dup rescue nil
70
+ end
71
+
72
+ def self.validate_arguments!
73
+ unless invalid_arguments.empty?
74
+ arguments = invalid_arguments.map {|arg| "\"#{arg}\""}
75
+ if arguments.length == 1
76
+ message = "Invalid argument: #{arguments.first}"
77
+ elsif arguments.length > 1
78
+ message = "Invalid arguments: "
79
+ message << arguments[0...-1].join(", ")
80
+ message << " and "
81
+ message << arguments[-1]
82
+ end
83
+ $stderr.puts(format_with_bang(message))
84
+ run(current_command, ["--help"])
85
+ exit(1)
86
+ end
87
+ end
88
+
89
+ def self.warnings
90
+ @warnings ||= []
91
+ end
92
+
93
+ def self.display_warnings
94
+ unless warnings.empty?
95
+ $stderr.puts(warnings.map {|warning| " ! #{warning}"}.join("\n"))
96
+ end
97
+ end
98
+
99
+ def self.global_option(name, *args, &blk)
100
+ # args.sort.reverse gives -l, --long order
101
+ global_options << { :name => name.to_s, :args => args.sort.reverse, :proc => blk }
102
+ end
103
+
104
+ global_option :app, "-a", "--app APP" do |app|
105
+ raise OptionParser::InvalidOption.new(app) if app == "pp"
106
+ end
107
+
108
+ global_option :confirm, "--confirm APP"
109
+ global_option :help, "-h", "--help"
110
+ global_option :remote, "-r", "--remote REMOTE"
111
+
112
+ def self.prepare_run(cmd, args=[])
113
+ command = parse(cmd)
114
+
115
+ if args.include?('-h') || args.include?('--help')
116
+ args.unshift(cmd) unless cmd =~ /^-.*/
117
+ cmd = 'help'
118
+ command = parse(cmd)
119
+ end
120
+
121
+ if cmd == '--version'
122
+ cmd = 'version'
123
+ command = parse(cmd)
124
+ end
125
+
126
+ @current_command = cmd
127
+ @anonymized_args, @normalized_args = [], []
128
+
129
+ opts = {}
130
+ invalid_options = []
131
+
132
+ parser = OptionParser.new do |parser|
133
+ # remove OptionParsers Officious['version'] to avoid conflicts
134
+ # see: https://github.com/ruby/ruby/blob/trunk/lib/optparse.rb#L814
135
+ parser.base.long.delete('version')
136
+ (global_options + (command && command[:options] || [])).each do |option|
137
+ parser.on(*option[:args]) do |value|
138
+ if option[:proc]
139
+ option[:proc].call(value)
140
+ end
141
+ opts[option[:name].gsub('-', '_').to_sym] = value
142
+ ARGV.join(' ') =~ /(#{option[:args].map {|arg| arg.split(' ', 2).first}.join('|')})/
143
+ @anonymized_args << "#{$1} _"
144
+ @normalized_args << "#{option[:args].last.split(' ', 2).first} _"
145
+ end
146
+ end
147
+ end
148
+
149
+ begin
150
+ parser.order!(args) do |nonopt|
151
+ invalid_options << nonopt
152
+ @anonymized_args << '!'
153
+ @normalized_args << '!'
154
+ end
155
+ rescue OptionParser::InvalidOption => ex
156
+ invalid_options << ex.args.first
157
+ @anonymized_args << '!'
158
+ @normalized_args << '!'
159
+ retry
160
+ end
161
+
162
+ args.concat(invalid_options)
163
+
164
+ @current_args = args
165
+ @current_options = opts
166
+ @invalid_arguments = invalid_options
167
+
168
+ @anonymous_command = [ARGV.first, *@anonymized_args].join(' ')
169
+ begin
170
+ usage_directory = "#{home_directory}/.azuki/usage"
171
+ FileUtils.mkdir_p(usage_directory)
172
+ usage_file = usage_directory << "/#{Azuki::VERSION}"
173
+ usage = if File.exists?(usage_file)
174
+ json_decode(File.read(usage_file))
175
+ else
176
+ {}
177
+ end
178
+ usage[@anonymous_command] ||= 0
179
+ usage[@anonymous_command] += 1
180
+ File.write(usage_file, json_encode(usage) + "\n")
181
+ rescue
182
+ # usage writing is not important, allow failures
183
+ end
184
+
185
+ if command
186
+ command_instance = command[:klass].new(args.dup, opts.dup)
187
+
188
+ if !@normalized_args.include?('--app _') && (implied_app = command_instance.app rescue nil)
189
+ @normalized_args << '--app _'
190
+ end
191
+ @normalized_command = [ARGV.first, @normalized_args.sort_by {|arg| arg.gsub('-', '')}].join(' ')
192
+
193
+ [ command_instance, command[:method] ]
194
+ else
195
+ error([
196
+ "`#{cmd}` is not a azuki command.",
197
+ suggestion(cmd, commands.keys + command_aliases.keys),
198
+ "See `azuki help` for a list of available commands."
199
+ ].compact.join("\n"))
200
+ end
201
+ end
202
+
203
+ def self.run(cmd, arguments=[])
204
+ begin
205
+ object, method = prepare_run(cmd, arguments.dup)
206
+ object.send(method)
207
+ rescue Interrupt, StandardError, SystemExit => error
208
+ # load likely error classes, as they may not be loaded yet due to defered loads
209
+ require 'azuki-api'
210
+ require 'rest_client'
211
+ raise(error)
212
+ end
213
+ rescue Azuki::API::Errors::Unauthorized, RestClient::Unauthorized
214
+ puts "Authentication failure"
215
+ unless ENV['AZUKI_API_KEY']
216
+ run "login"
217
+ retry
218
+ end
219
+ rescue Azuki::API::Errors::VerificationRequired, RestClient::PaymentRequired => e
220
+ retry if Azuki::Helpers.confirm_billing
221
+ rescue Azuki::API::Errors::NotFound => e
222
+ error extract_error(e.response.body) {
223
+ e.response.body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found"
224
+ }
225
+ rescue RestClient::ResourceNotFound => e
226
+ error extract_error(e.http_body) {
227
+ e.http_body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found"
228
+ }
229
+ rescue Azuki::API::Errors::Locked => e
230
+ app = e.response.headers[:x_confirmation_required]
231
+ if confirm_command(app, extract_error(e.response.body))
232
+ arguments << '--confirm' << app
233
+ retry
234
+ end
235
+ rescue RestClient::Locked => e
236
+ app = e.response.headers[:x_confirmation_required]
237
+ if confirm_command(app, extract_error(e.http_body))
238
+ arguments << '--confirm' << app
239
+ retry
240
+ end
241
+ rescue Azuki::API::Errors::Timeout, RestClient::RequestTimeout
242
+ error "API request timed out. Please try again, or contact support@azukiapp.com if this issue persists."
243
+ rescue Azuki::API::Errors::ErrorWithResponse => e
244
+ error extract_error(e.response.body)
245
+ rescue RestClient::RequestFailed => e
246
+ error extract_error(e.http_body)
247
+ rescue CommandFailed => e
248
+ error e.message
249
+ rescue OptionParser::ParseError
250
+ commands[cmd] ? run("help", [cmd]) : run("help")
251
+ rescue Excon::Errors::SocketError => e
252
+ if e.message == 'getaddrinfo: nodename nor servname provided, or not known (SocketError)'
253
+ error("Unable to connect to Azuki API, please check internet connectivity and try again.")
254
+ else
255
+ raise(e)
256
+ end
257
+ ensure
258
+ display_warnings
259
+ end
260
+
261
+ def self.parse(cmd)
262
+ commands[cmd] || commands[command_aliases[cmd]]
263
+ end
264
+
265
+ def self.extract_error(body, options={})
266
+ default_error = block_given? ? yield : "Internal server error.\nRun `azuki status` to check for known platform issues."
267
+ parse_error_xml(body) || parse_error_json(body) || parse_error_plain(body) || default_error
268
+ end
269
+
270
+ def self.parse_error_xml(body)
271
+ xml_errors = REXML::Document.new(body).elements.to_a("//errors/error")
272
+ msg = xml_errors.map { |a| a.text }.join(" / ")
273
+ return msg unless msg.empty?
274
+ rescue Exception
275
+ end
276
+
277
+ def self.parse_error_json(body)
278
+ json = json_decode(body.to_s) rescue false
279
+ case json
280
+ when Array
281
+ json.first.join(' ') # message like [['base', 'message']]
282
+ when Hash
283
+ json['error'] # message like {'error' => 'message'}
284
+ else
285
+ nil
286
+ end
287
+ end
288
+
289
+ def self.parse_error_plain(body)
290
+ return unless body.respond_to?(:headers) && body.headers[:content_type].to_s.include?("text/plain")
291
+ body.to_s
292
+ end
293
+ end
294
+ end