azuki 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +71 -0
- data/bin/azuki +17 -0
- data/data/cacert.pem +3988 -0
- data/lib/azuki.rb +17 -0
- data/lib/azuki/auth.rb +339 -0
- data/lib/azuki/cli.rb +38 -0
- data/lib/azuki/client.rb +764 -0
- data/lib/azuki/client/azuki_postgresql.rb +141 -0
- data/lib/azuki/client/cisaurus.rb +26 -0
- data/lib/azuki/client/pgbackups.rb +113 -0
- data/lib/azuki/client/rendezvous.rb +108 -0
- data/lib/azuki/client/ssl_endpoint.rb +25 -0
- data/lib/azuki/command.rb +294 -0
- data/lib/azuki/command/account.rb +23 -0
- data/lib/azuki/command/accounts.rb +34 -0
- data/lib/azuki/command/addons.rb +305 -0
- data/lib/azuki/command/apps.rb +393 -0
- data/lib/azuki/command/auth.rb +86 -0
- data/lib/azuki/command/base.rb +230 -0
- data/lib/azuki/command/certs.rb +209 -0
- data/lib/azuki/command/config.rb +137 -0
- data/lib/azuki/command/db.rb +218 -0
- data/lib/azuki/command/domains.rb +85 -0
- data/lib/azuki/command/drains.rb +46 -0
- data/lib/azuki/command/fork.rb +164 -0
- data/lib/azuki/command/git.rb +64 -0
- data/lib/azuki/command/help.rb +179 -0
- data/lib/azuki/command/keys.rb +115 -0
- data/lib/azuki/command/labs.rb +147 -0
- data/lib/azuki/command/logs.rb +45 -0
- data/lib/azuki/command/maintenance.rb +61 -0
- data/lib/azuki/command/pg.rb +269 -0
- data/lib/azuki/command/pgbackups.rb +329 -0
- data/lib/azuki/command/plugins.rb +110 -0
- data/lib/azuki/command/ps.rb +232 -0
- data/lib/azuki/command/regions.rb +22 -0
- data/lib/azuki/command/releases.rb +124 -0
- data/lib/azuki/command/run.rb +180 -0
- data/lib/azuki/command/sharing.rb +89 -0
- data/lib/azuki/command/ssl.rb +43 -0
- data/lib/azuki/command/stack.rb +62 -0
- data/lib/azuki/command/status.rb +51 -0
- data/lib/azuki/command/update.rb +47 -0
- data/lib/azuki/command/version.rb +23 -0
- data/lib/azuki/deprecated.rb +5 -0
- data/lib/azuki/deprecated/help.rb +38 -0
- data/lib/azuki/distribution.rb +9 -0
- data/lib/azuki/excon.rb +9 -0
- data/lib/azuki/helpers.rb +517 -0
- data/lib/azuki/helpers/azuki_postgresql.rb +165 -0
- data/lib/azuki/helpers/log_displayer.rb +70 -0
- data/lib/azuki/plugin.rb +163 -0
- data/lib/azuki/updater.rb +171 -0
- data/lib/azuki/version.rb +3 -0
- data/lib/vendor/azuki/okjson.rb +598 -0
- data/spec/azuki/auth_spec.rb +256 -0
- data/spec/azuki/client/azuki_postgresql_spec.rb +71 -0
- data/spec/azuki/client/pgbackups_spec.rb +43 -0
- data/spec/azuki/client/rendezvous_spec.rb +62 -0
- data/spec/azuki/client/ssl_endpoint_spec.rb +48 -0
- data/spec/azuki/client_spec.rb +564 -0
- data/spec/azuki/command/addons_spec.rb +601 -0
- data/spec/azuki/command/apps_spec.rb +351 -0
- data/spec/azuki/command/auth_spec.rb +38 -0
- data/spec/azuki/command/base_spec.rb +109 -0
- data/spec/azuki/command/certs_spec.rb +178 -0
- data/spec/azuki/command/config_spec.rb +144 -0
- data/spec/azuki/command/db_spec.rb +110 -0
- data/spec/azuki/command/domains_spec.rb +87 -0
- data/spec/azuki/command/drains_spec.rb +34 -0
- data/spec/azuki/command/fork_spec.rb +56 -0
- data/spec/azuki/command/git_spec.rb +144 -0
- data/spec/azuki/command/help_spec.rb +93 -0
- data/spec/azuki/command/keys_spec.rb +120 -0
- data/spec/azuki/command/labs_spec.rb +100 -0
- data/spec/azuki/command/logs_spec.rb +60 -0
- data/spec/azuki/command/maintenance_spec.rb +51 -0
- data/spec/azuki/command/pg_spec.rb +236 -0
- data/spec/azuki/command/pgbackups_spec.rb +307 -0
- data/spec/azuki/command/plugins_spec.rb +104 -0
- data/spec/azuki/command/ps_spec.rb +195 -0
- data/spec/azuki/command/releases_spec.rb +130 -0
- data/spec/azuki/command/run_spec.rb +83 -0
- data/spec/azuki/command/sharing_spec.rb +59 -0
- data/spec/azuki/command/stack_spec.rb +46 -0
- data/spec/azuki/command/status_spec.rb +48 -0
- data/spec/azuki/command/version_spec.rb +16 -0
- data/spec/azuki/command_spec.rb +211 -0
- data/spec/azuki/helpers/azuki_postgresql_spec.rb +155 -0
- data/spec/azuki/helpers_spec.rb +48 -0
- data/spec/azuki/plugin_spec.rb +172 -0
- data/spec/azuki/updater_spec.rb +44 -0
- data/spec/helper/legacy_help.rb +16 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +224 -0
- data/spec/support/display_message_matcher.rb +49 -0
- data/spec/support/openssl_mock_helper.rb +8 -0
- 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
|