aptible-cli 0.16.2 → 0.16.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/README.md +48 -45
- data/aptible-cli.gemspec +2 -2
- data/lib/aptible/cli/agent.rb +78 -9
- data/lib/aptible/cli/helpers/database.rb +23 -0
- data/lib/aptible/cli/helpers/security_key.rb +136 -0
- data/lib/aptible/cli/helpers/system.rb +26 -0
- data/lib/aptible/cli/resource_formatter.rb +36 -1
- data/lib/aptible/cli/subcommands/backup.rb +56 -12
- data/lib/aptible/cli/subcommands/db.rb +66 -6
- data/lib/aptible/cli/subcommands/inspect.rb +1 -1
- data/lib/aptible/cli/version.rb +1 -1
- data/spec/aptible/cli/agent_spec.rb +131 -9
- data/spec/aptible/cli/subcommands/backup_spec.rb +79 -2
- data/spec/aptible/cli/subcommands/db_spec.rb +147 -2
- metadata +9 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 35f34899ac1d54918f9f5852b665196f65f8a40daa70a405ffd10c64f0a6014a
|
4
|
+
data.tar.gz: 8c0703501a28277b3775fe85b80b53ccdfc9a36256073da64119995f84b17534
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3da14de30e9885ee58a3d03141a6c854beaf499825faac9dae1deeebb676ceab16ee57b0a68f6148592ccca8b87d385c3052c426cf6fc028d34b86eb84d3b8d2
|
7
|
+
data.tar.gz: 634ece7c98758d5164ef56fa5a82bfbbe7974568f5d806d8e93cfd4e50c027ec7834785eaebb05307eb00dfa2a6e9b769b5b20c1415f84c4b026e73c5a14bbe1
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -29,51 +29,54 @@ From `aptible help`:
|
|
29
29
|
<!-- BEGIN USAGE -->
|
30
30
|
```
|
31
31
|
Commands:
|
32
|
-
aptible apps
|
33
|
-
aptible apps:create HANDLE
|
34
|
-
aptible apps:deprovision
|
35
|
-
aptible apps:scale SERVICE [--container-count COUNT] [--container-size SIZE_MB]
|
36
|
-
aptible backup:list DB_HANDLE
|
37
|
-
aptible backup:
|
38
|
-
aptible
|
39
|
-
aptible
|
40
|
-
aptible config
|
41
|
-
aptible config:
|
42
|
-
aptible config:
|
43
|
-
aptible
|
44
|
-
aptible
|
45
|
-
aptible db:
|
46
|
-
aptible db:
|
47
|
-
aptible db:
|
48
|
-
aptible db:
|
49
|
-
aptible db:
|
50
|
-
aptible db:
|
51
|
-
aptible db:
|
52
|
-
aptible db:
|
53
|
-
aptible db:
|
54
|
-
aptible db:
|
55
|
-
aptible
|
56
|
-
aptible
|
57
|
-
aptible
|
58
|
-
aptible
|
59
|
-
aptible
|
60
|
-
aptible endpoints:
|
61
|
-
aptible endpoints:
|
62
|
-
aptible endpoints:
|
63
|
-
aptible endpoints:
|
64
|
-
aptible endpoints:
|
65
|
-
aptible endpoints:
|
66
|
-
aptible endpoints:
|
67
|
-
aptible
|
68
|
-
aptible
|
69
|
-
aptible
|
70
|
-
aptible
|
71
|
-
aptible
|
72
|
-
aptible
|
73
|
-
aptible
|
74
|
-
aptible
|
75
|
-
aptible
|
76
|
-
aptible
|
32
|
+
aptible apps # List all applications
|
33
|
+
aptible apps:create HANDLE # Create a new application
|
34
|
+
aptible apps:deprovision # Deprovision an app
|
35
|
+
aptible apps:scale SERVICE [--container-count COUNT] [--container-size SIZE_MB] # Scale a service
|
36
|
+
aptible backup:list DB_HANDLE # List backups for a database
|
37
|
+
aptible backup:orphaned # List backups associated with deprovisioned databases
|
38
|
+
aptible backup:purge BACKUP_ID # Permanently delete a backup and any copies of it
|
39
|
+
aptible backup:restore BACKUP_ID [--environment ENVIRONMENT_HANDLE] [--handle HANDLE] [--container-size SIZE_MB] [--disk-size SIZE_GB] # Restore a backup
|
40
|
+
aptible config # Print an app's current configuration
|
41
|
+
aptible config:add [VAR1=VAL1] [VAR2=VAL2] [...] # Add an ENV variable to an app
|
42
|
+
aptible config:rm [VAR1] [VAR2] [...] # Remove an ENV variable from an app
|
43
|
+
aptible config:set [VAR1=VAL1] [VAR2=VAL2] [...] # Add an ENV variable to an app
|
44
|
+
aptible config:unset [VAR1] [VAR2] [...] # Remove an ENV variable from an app
|
45
|
+
aptible db:backup HANDLE # Backup a database
|
46
|
+
aptible db:clone SOURCE DEST # Clone a database to create a new one
|
47
|
+
aptible db:create HANDLE [--type TYPE] [--version VERSION] [--container-size SIZE_MB] [--disk-size SIZE_GB] # Create a new database
|
48
|
+
aptible db:deprovision HANDLE # Deprovision a database
|
49
|
+
aptible db:dump HANDLE [pg_dump options] # Dump a remote database to file
|
50
|
+
aptible db:execute HANDLE SQL_FILE [--on-error-stop] # Executes sql against a database
|
51
|
+
aptible db:list # List all databases
|
52
|
+
aptible db:reload HANDLE # Reload a database
|
53
|
+
aptible db:replicate HANDLE REPLICA_HANDLE [--container-size SIZE_MB] [--disk-size SIZE_GB] [--logical --version VERSION] # Create a replica/follower of a database
|
54
|
+
aptible db:restart HANDLE [--container-size SIZE_MB] [--disk-size SIZE_GB] # Restart a database
|
55
|
+
aptible db:tunnel HANDLE # Create a local tunnel to a database
|
56
|
+
aptible db:url HANDLE # Display a database URL
|
57
|
+
aptible db:versions # List available database versions
|
58
|
+
aptible deploy [OPTIONS] [VAR1=VAL1] [VAR2=VAL2] [...] # Deploy an app
|
59
|
+
aptible domains # Print an app's current virtual domains - DEPRECATED
|
60
|
+
aptible endpoints:database:create DATABASE # Create a Database Endpoint
|
61
|
+
aptible endpoints:deprovision [--app APP | --database DATABASE] ENDPOINT_HOSTNAME # Deprovision an App or Database Endpoint
|
62
|
+
aptible endpoints:https:create [--app APP] SERVICE # Create an App HTTPS Endpoint
|
63
|
+
aptible endpoints:https:modify [--app APP] ENDPOINT_HOSTNAME # Modify an App HTTPS Endpoint
|
64
|
+
aptible endpoints:list [--app APP | --database DATABASE] # List Endpoints for an App or Database
|
65
|
+
aptible endpoints:renew [--app APP] ENDPOINT_HOSTNAME # Renew an App Managed TLS Endpoint
|
66
|
+
aptible endpoints:tcp:create [--app APP] SERVICE # Create an App TCP Endpoint
|
67
|
+
aptible endpoints:tcp:modify [--app APP] ENDPOINT_HOSTNAME # Modify an App TCP Endpoint
|
68
|
+
aptible endpoints:tls:create [--app APP] SERVICE # Create an App TLS Endpoint
|
69
|
+
aptible endpoints:tls:modify [--app APP] ENDPOINT_HOSTNAME # Modify an App TLS Endpoint
|
70
|
+
aptible help [COMMAND] # Describe available commands or one specific command
|
71
|
+
aptible login # Log in to Aptible
|
72
|
+
aptible logs [--app APP | --database DATABASE] # Follows logs from a running app or database
|
73
|
+
aptible operation:cancel OPERATION_ID # Cancel a running operation
|
74
|
+
aptible ps # Display running processes for an app - DEPRECATED
|
75
|
+
aptible rebuild # Rebuild an app, and restart its services
|
76
|
+
aptible restart # Restart all services associated with an app
|
77
|
+
aptible services # List Services for an App
|
78
|
+
aptible ssh [COMMAND] # Run a command against an app
|
79
|
+
aptible version # Print Aptible CLI version
|
77
80
|
```
|
78
81
|
<!-- END USAGE -->
|
79
82
|
|
data/aptible-cli.gemspec
CHANGED
@@ -21,8 +21,8 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.require_paths = ['lib']
|
22
22
|
|
23
23
|
spec.add_dependency 'aptible-resource', '~> 1.1'
|
24
|
-
spec.add_dependency 'aptible-api', '~> 1.
|
25
|
-
spec.add_dependency 'aptible-auth', '~> 1.0'
|
24
|
+
spec.add_dependency 'aptible-api', '~> 1.2'
|
25
|
+
spec.add_dependency 'aptible-auth', '~> 1.1.0'
|
26
26
|
spec.add_dependency 'aptible-billing', '~> 1.0'
|
27
27
|
spec.add_dependency 'thor', '~> 0.20.0'
|
28
28
|
spec.add_dependency 'git'
|
data/lib/aptible/cli/agent.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'base64'
|
1
2
|
require 'uri'
|
2
3
|
|
3
4
|
require 'aptible/auth'
|
@@ -15,6 +16,8 @@ require_relative 'helpers/app_or_database'
|
|
15
16
|
require_relative 'helpers/vhost'
|
16
17
|
require_relative 'helpers/vhost/option_set_builder'
|
17
18
|
require_relative 'helpers/tunnel'
|
19
|
+
require_relative 'helpers/system'
|
20
|
+
require_relative 'helpers/security_key'
|
18
21
|
|
19
22
|
require_relative 'subcommands/apps'
|
20
23
|
require_relative 'subcommands/config'
|
@@ -39,6 +42,7 @@ module Aptible
|
|
39
42
|
|
40
43
|
include Helpers::Token
|
41
44
|
include Helpers::Ssh
|
45
|
+
include Helpers::System
|
42
46
|
include Subcommands::Apps
|
43
47
|
include Subcommands::Config
|
44
48
|
include Subcommands::DB
|
@@ -81,10 +85,26 @@ module Aptible
|
|
81
85
|
option :lifetime, desc: 'The duration the token should be valid for ' \
|
82
86
|
'(example usage: 24h, 1d, 600s, etc.)'
|
83
87
|
option :otp_token, desc: 'A token generated by your second-factor app'
|
88
|
+
option :sso, desc: 'Use a token from a Single Sign On login on the ' \
|
89
|
+
'dashboard'
|
84
90
|
def login
|
91
|
+
if options[:sso]
|
92
|
+
begin
|
93
|
+
token = options[:sso]
|
94
|
+
token = ask('Paste token copied from Dashboard:') if token == 'sso'
|
95
|
+
Base64.urlsafe_decode64(token.split('.').first)
|
96
|
+
save_token(token)
|
97
|
+
CLI.logger.info "Token written to #{token_file}"
|
98
|
+
return
|
99
|
+
rescue StandardError
|
100
|
+
raise Thor::Error, 'Invalid token provided for SSO'
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
85
104
|
email = options[:email] || ask('Email: ')
|
86
|
-
password = options[:password] ||
|
87
|
-
|
105
|
+
password = options[:password] || ask_then_line(
|
106
|
+
'Password: ', echo: false
|
107
|
+
)
|
88
108
|
|
89
109
|
token_options = { email: email, password: password }
|
90
110
|
|
@@ -93,7 +113,7 @@ module Aptible
|
|
93
113
|
|
94
114
|
begin
|
95
115
|
lifetime = '1w'
|
96
|
-
lifetime = '12h' if token_options[:otp_token]
|
116
|
+
lifetime = '12h' if token_options[:otp_token] || token_options[:u2f]
|
97
117
|
lifetime = options[:lifetime] if options[:lifetime]
|
98
118
|
|
99
119
|
duration = ChronicDuration.parse(lifetime)
|
@@ -104,14 +124,63 @@ module Aptible
|
|
104
124
|
token_options[:expires_in] = duration
|
105
125
|
token = Aptible::Auth::Token.create(token_options)
|
106
126
|
rescue OAuth2::Error => e
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
127
|
+
# If a MFA is require but a token wasn't provided,
|
128
|
+
# prompt the user for MFA authentication and retry
|
129
|
+
if e.code != 'otp_token_required'
|
130
|
+
raise Thor::Error, 'Could not authenticate with given ' \
|
131
|
+
"credentials: #{e.code}"
|
132
|
+
end
|
133
|
+
|
134
|
+
u2f = (e.response.parsed['exception_context'] || {})['u2f']
|
135
|
+
|
136
|
+
q = Queue.new
|
137
|
+
mfa_threads = []
|
138
|
+
|
139
|
+
# If the user has added a security key and their computer supports it,
|
140
|
+
# allow them to use it
|
141
|
+
if u2f && !which('u2f-host').nil?
|
142
|
+
origin = Aptible::Auth::Resource.new.get.href
|
143
|
+
app_id = Aptible::Auth::Resource.new.utf_trusted_facets.href
|
144
|
+
|
145
|
+
challenge = u2f.fetch('challenge')
|
146
|
+
|
147
|
+
devices = u2f.fetch('devices').map do |dev|
|
148
|
+
Helpers::SecurityKey::Device.new(
|
149
|
+
dev.fetch('version'), dev.fetch('key_handle')
|
150
|
+
)
|
151
|
+
end
|
152
|
+
|
153
|
+
puts 'Enter your 2FA token or touch your Security Key once it ' \
|
154
|
+
'starts blinking.'
|
155
|
+
|
156
|
+
mfa_threads << Thread.new do
|
157
|
+
token_options[:u2f] = Helpers::SecurityKey.authenticate(
|
158
|
+
origin, app_id, challenge, devices
|
159
|
+
)
|
160
|
+
|
161
|
+
puts ''
|
162
|
+
|
163
|
+
q.push(nil)
|
164
|
+
end
|
111
165
|
end
|
112
166
|
|
113
|
-
|
114
|
-
|
167
|
+
mfa_threads << Thread.new do
|
168
|
+
token_options[:otp_token] = options[:otp_token] || ask(
|
169
|
+
'2FA Token: '
|
170
|
+
)
|
171
|
+
|
172
|
+
q.push(nil)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Block until one of the threads completes
|
176
|
+
q.pop
|
177
|
+
|
178
|
+
mfa_threads.each do |thr|
|
179
|
+
sleep 0.5 until thr.status != 'run'
|
180
|
+
thr.kill
|
181
|
+
end.each(&:join)
|
182
|
+
|
183
|
+
retry
|
115
184
|
end
|
116
185
|
|
117
186
|
save_token(token.access_token)
|
@@ -47,6 +47,29 @@ module Aptible
|
|
47
47
|
databases_from_handle(dest_handle, source.account).first
|
48
48
|
end
|
49
49
|
|
50
|
+
def replicate_database(source, dest_handle, options)
|
51
|
+
replication_params = {
|
52
|
+
handle: dest_handle,
|
53
|
+
container_size: options[:container_size],
|
54
|
+
disk_size: options[:size]
|
55
|
+
}.reject { |_, v| v.nil? }
|
56
|
+
|
57
|
+
if options[:logical]
|
58
|
+
replication_params[:type] = 'replicate_logical'
|
59
|
+
replication_params[:docker_ref] =
|
60
|
+
options[:database_image].docker_repo
|
61
|
+
else
|
62
|
+
replication_params[:type] = 'replicate'
|
63
|
+
end
|
64
|
+
|
65
|
+
op = source.create_operation!(replication_params)
|
66
|
+
attach_to_operation_logs(op)
|
67
|
+
|
68
|
+
replica = databases_from_handle(dest_handle, source.account).first
|
69
|
+
attach_to_operation_logs(replica.operations.last)
|
70
|
+
replica
|
71
|
+
end
|
72
|
+
|
50
73
|
# Creates a local tunnel and yields the helper
|
51
74
|
|
52
75
|
def with_local_tunnel(credential, port = 0)
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Aptible
|
2
|
+
module CLI
|
3
|
+
module Helpers
|
4
|
+
module SecurityKey
|
5
|
+
U2F_LOGGER = Logger.new(
|
6
|
+
ENV['U2F_DEBUG'] ? STDERR : File.open(File::NULL, 'w')
|
7
|
+
)
|
8
|
+
|
9
|
+
class AuthenticatorParameters
|
10
|
+
attr_reader :origin, :challenge, :app_id, :version, :key_handle
|
11
|
+
attr_reader :request
|
12
|
+
|
13
|
+
def initialize(origin, challenge, app_id, device)
|
14
|
+
@origin = origin
|
15
|
+
@challenge = challenge
|
16
|
+
@app_id = app_id
|
17
|
+
@version = device.version
|
18
|
+
@key_handle = device.key_handle
|
19
|
+
|
20
|
+
@request = {
|
21
|
+
'challenge' => challenge,
|
22
|
+
'appId' => app_id,
|
23
|
+
'version' => version,
|
24
|
+
'keyHandle' => key_handle
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class ThrottledAuthenticator
|
30
|
+
attr_reader :pid
|
31
|
+
|
32
|
+
def initialize(auth, pid)
|
33
|
+
@auth = auth
|
34
|
+
@pid = pid
|
35
|
+
end
|
36
|
+
|
37
|
+
def exited(_status)
|
38
|
+
[Authenticator.spawn(@auth), nil]
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.spawn(auth)
|
42
|
+
pid = Process.spawn(
|
43
|
+
'sleep', '2',
|
44
|
+
in: :close, out: :close, err: :close,
|
45
|
+
close_others: true
|
46
|
+
)
|
47
|
+
|
48
|
+
U2F_LOGGER.debug("#{self} #{auth.key_handle}: spawned #{pid}")
|
49
|
+
|
50
|
+
new(auth, pid)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Authenticator
|
55
|
+
attr_reader :pid
|
56
|
+
|
57
|
+
def initialize(auth, pid, out_read, err_read)
|
58
|
+
@auth = auth
|
59
|
+
@pid = pid
|
60
|
+
@out_read = out_read
|
61
|
+
@err_read = err_read
|
62
|
+
end
|
63
|
+
|
64
|
+
def exited(status)
|
65
|
+
out, err = [@out_read, @err_read].map(&:read).map(&:chomp)
|
66
|
+
|
67
|
+
if status.exitstatus == 0
|
68
|
+
U2F_LOGGER.info("#{self.class} #{@auth.key_handle}: ok: #{out}")
|
69
|
+
[nil, JSON.parse(out)]
|
70
|
+
else
|
71
|
+
U2F_LOGGER.warn("#{self.class} #{@auth.key_handle}: err: #{err}")
|
72
|
+
[ThrottledAuthenticator.spawn(@auth), nil]
|
73
|
+
end
|
74
|
+
ensure
|
75
|
+
[@out_read, @err_read].each(&:close)
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.spawn(auth)
|
79
|
+
in_read, in_write = IO.pipe
|
80
|
+
out_read, out_write = IO.pipe
|
81
|
+
err_read, err_write = IO.pipe
|
82
|
+
|
83
|
+
pid = Process.spawn(
|
84
|
+
'u2f-host', '-aauthenticate', '-o', auth.origin,
|
85
|
+
in: in_read, out: out_write, err: err_write,
|
86
|
+
close_others: true
|
87
|
+
)
|
88
|
+
|
89
|
+
U2F_LOGGER.debug("#{self} #{auth.key_handle}: spawned #{pid}")
|
90
|
+
|
91
|
+
[in_read, out_write, err_write].each(&:close)
|
92
|
+
|
93
|
+
in_write.write(auth.request.to_json)
|
94
|
+
in_write.close
|
95
|
+
|
96
|
+
new(auth, pid, out_read, err_read)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class Device
|
101
|
+
attr_reader :version, :key_handle
|
102
|
+
|
103
|
+
def initialize(version, key_handle)
|
104
|
+
@version = version
|
105
|
+
@key_handle = key_handle
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.authenticate(origin, app_id, challenge, devices)
|
110
|
+
procs = Hash[devices.map do |device|
|
111
|
+
params = AuthenticatorParameters.new(
|
112
|
+
origin, challenge, app_id, device
|
113
|
+
)
|
114
|
+
w = Authenticator.spawn(params)
|
115
|
+
[w.pid, w]
|
116
|
+
end]
|
117
|
+
|
118
|
+
begin
|
119
|
+
loop do
|
120
|
+
pid, status = Process.wait2
|
121
|
+
w = procs.delete(pid)
|
122
|
+
raise "waited unknown pid: #{pid}" if w.nil?
|
123
|
+
|
124
|
+
r, out = w.exited(status)
|
125
|
+
|
126
|
+
procs[r.pid] = r if r
|
127
|
+
return out if out
|
128
|
+
end
|
129
|
+
ensure
|
130
|
+
procs.values.map(&:pid).each { |p| Process.kill(:SIGTERM, p) }
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Aptible
|
2
|
+
module CLI
|
3
|
+
module Helpers
|
4
|
+
module System
|
5
|
+
def which(cmd)
|
6
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
7
|
+
|
8
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
9
|
+
exts.each do |ext|
|
10
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
11
|
+
return exe if File.executable?(exe) && !File.directory?(exe)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def ask_then_line(*args)
|
19
|
+
ret = ask(*args)
|
20
|
+
puts ''
|
21
|
+
ret
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|