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