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