aptible-cli 0.16.2 → 0.16.7
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 +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
|