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