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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c50391308952a422d84f497808c25fbaf4c405730497266a299e69b632f37887
4
- data.tar.gz: 6146d0c3dc58d7ef9786f95a341a22d500689c42f67ed48c0b824f62a2f250c6
3
+ metadata.gz: 35f34899ac1d54918f9f5852b665196f65f8a40daa70a405ffd10c64f0a6014a
4
+ data.tar.gz: 8c0703501a28277b3775fe85b80b53ccdfc9a36256073da64119995f84b17534
5
5
  SHA512:
6
- metadata.gz: 156abd2537b61dd8416ce75f6b8389e807306869254bef47db51c80e081a433e3b802483327748c0bb18c9c5691dd537fc14f4f536c55505f01e8c7aa2a86849
7
- data.tar.gz: eec30d958642888b92418343ded298ba9ddb49b370395795281e4390494c0a480c771b202b0e7845c1a953c43354cb9aa3404f31589146741fe6d6e2c91f1707
6
+ metadata.gz: 3da14de30e9885ee58a3d03141a6c854beaf499825faac9dae1deeebb676ceab16ee57b0a68f6148592ccca8b87d385c3052c426cf6fc028d34b86eb84d3b8d2
7
+ data.tar.gz: 634ece7c98758d5164ef56fa5a82bfbbe7974568f5d806d8e93cfd4e50c027ec7834785eaebb05307eb00dfa2a6e9b769b5b20c1415f84c4b026e73c5a14bbe1
data/Gemfile CHANGED
@@ -6,7 +6,7 @@ gem 'rack', '~> 1.0'
6
6
 
7
7
  group :test do
8
8
  gem 'webmock'
9
- gem 'codecov', require: false
9
+ gem 'codecov', '~> 0.1.0', require: false
10
10
  end
11
11
 
12
12
  # Specify your gem's dependencies in aptible-cli.gemspec
data/README.md CHANGED
@@ -29,51 +29,54 @@ From `aptible help`:
29
29
  <!-- BEGIN USAGE -->
30
30
  ```
31
31
  Commands:
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:restore BACKUP_ID [--environment ENVIRONMENT_HANDLE] [--handle HANDLE] [--container-size SIZE_MB] [--size SIZE_GB] # Restore a backup
38
- aptible config # Print an app's current configuration
39
- aptible config:add [VAR1=VAL1] [VAR2=VAL2] [...] # Add an ENV variable to an app
40
- aptible config:rm [VAR1] [VAR2] [...] # Remove an ENV variable from an app
41
- aptible config:set [VAR1=VAL1] [VAR2=VAL2] [...] # Add an ENV variable to an app
42
- aptible config:unset [VAR1] [VAR2] [...] # Remove an ENV variable from an app
43
- aptible db:backup HANDLE # Backup a database
44
- aptible db:clone SOURCE DEST # Clone a database to create a new one
45
- aptible db:create HANDLE [--type TYPE] [--version VERSION] [--container-size SIZE_MB] [--size SIZE_GB] # Create a new database
46
- aptible db:deprovision HANDLE # Deprovision a database
47
- aptible db:dump HANDLE [pg_dump options] # Dump a remote database to file
48
- aptible db:execute HANDLE SQL_FILE [--on-error-stop] # Executes sql against a database
49
- aptible db:list # List all databases
50
- aptible db:reload HANDLE # Reload a database
51
- aptible db:restart HANDLE [--container-size SIZE_MB] [--size SIZE_GB] # Restart a database
52
- aptible db:tunnel HANDLE # Create a local tunnel to a database
53
- aptible db:url HANDLE # Display a database URL
54
- aptible db:versions # List available database versions
55
- aptible deploy [OPTIONS] [VAR1=VAL1] [VAR2=VAL2] [...] # Deploy an app
56
- aptible domains # Print an app's current virtual domains - DEPRECATED
57
- aptible endpoints:database:create DATABASE # Create a Database Endpoint
58
- aptible endpoints:deprovision [--app APP | --database DATABASE] ENDPOINT_HOSTNAME # Deprovision an App or Database Endpoint
59
- aptible endpoints:https:create [--app APP] SERVICE # Create an App HTTPS Endpoint
60
- aptible endpoints:https:modify [--app APP] ENDPOINT_HOSTNAME # Modify an App HTTPS Endpoint
61
- aptible endpoints:list [--app APP | --database DATABASE] # List Endpoints for an App or Database
62
- aptible endpoints:renew [--app APP] ENDPOINT_HOSTNAME # Renew an App Managed TLS Endpoint
63
- aptible endpoints:tcp:create [--app APP] SERVICE # Create an App TCP Endpoint
64
- aptible endpoints:tcp:modify [--app APP] ENDPOINT_HOSTNAME # Modify an App TCP Endpoint
65
- aptible endpoints:tls:create [--app APP] SERVICE # Create an App TLS Endpoint
66
- aptible endpoints:tls:modify [--app APP] ENDPOINT_HOSTNAME # Modify an App TLS Endpoint
67
- aptible help [COMMAND] # Describe available commands or one specific command
68
- aptible login # Log in to Aptible
69
- aptible logs [--app APP | --database DATABASE] # Follows logs from a running app or database
70
- aptible operation:cancel OPERATION_ID # Cancel a running operation
71
- aptible ps # Display running processes for an app - DEPRECATED
72
- aptible rebuild # Rebuild an app, and restart its services
73
- aptible restart # Restart all services associated with an app
74
- aptible services # List Services for an App
75
- aptible ssh [COMMAND] # Run a command against an app
76
- aptible version # Print Aptible CLI version
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
 
@@ -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.0'
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'
@@ -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] || ask('Password: ', echo: false)
87
- puts ''
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
- if e.code == 'otp_token_required'
108
- token_options[:otp_token] = options[:otp_token] ||
109
- ask('2FA Token: ')
110
- retry
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
- raise Thor::Error, 'Could not authenticate with given credentials: ' \
114
- "#{e.code}"
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