aptible-cli 0.7.5 → 0.8.0
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/aptible-cli.gemspec +2 -2
- data/lib/aptible/cli/agent.rb +3 -0
- data/lib/aptible/cli/helpers/database.rb +12 -19
- data/lib/aptible/cli/helpers/operation.rb +23 -12
- data/lib/aptible/cli/helpers/ssh.rb +67 -9
- data/lib/aptible/cli/helpers/tunnel.rb +4 -19
- data/lib/aptible/cli/subcommands/logs.rb +20 -14
- data/lib/aptible/cli/subcommands/operation.rb +23 -0
- data/lib/aptible/cli/subcommands/ps.rb +5 -10
- data/lib/aptible/cli/subcommands/ssh.rb +7 -12
- data/lib/aptible/cli/version.rb +1 -1
- data/spec/aptible/cli/helpers/operation_spec.rb +23 -0
- data/spec/aptible/cli/helpers/ssh_spec.rb +92 -0
- data/spec/aptible/cli/helpers/tunnel_spec.rb +12 -20
- data/spec/aptible/cli/subcommands/logs_spec.rb +46 -13
- data/spec/aptible/cli/subcommands/operation_spec.rb +30 -0
- data/spec/mock/ssh +5 -12
- data/spec/mock/ssh_mock.rb +5 -12
- metadata +12 -7
- data/spec/aptible/cli/subcommands/ps_spec.rb +0 -36
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe2b01937188ac5154d4b1f3ad75c4b614099f6b
|
4
|
+
data.tar.gz: 34b46d94e014464efab70894071fbe79e0db388d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 621652b7947a133f0c5a3981238f1537eba48b18db5f45dbbd26ad492c6480636b76086b7ceb7541df789469770aef204fcf98c996b757d6c1aa63e74a377193
|
7
|
+
data.tar.gz: f7497688bb7fa6a4c9c612b8414d2afe040fb1a66e350be1042f6e135728b6120a0831c0b14e00bea9b38325006f2c1d2c060a3ffae3d0129bb6537f41e26d28
|
data/aptible-cli.gemspec
CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.test_files = spec.files.grep(%r{spec/})
|
21
21
|
spec.require_paths = ['lib']
|
22
22
|
|
23
|
-
spec.add_dependency 'aptible-api', '~> 0.9.
|
23
|
+
spec.add_dependency 'aptible-api', '~> 0.9.14'
|
24
24
|
spec.add_dependency 'aptible-auth', '~> 0.11.8'
|
25
25
|
spec.add_dependency 'aptible-resource', '~> 0.3.6'
|
26
26
|
spec.add_dependency 'thor', '~> 0.19.1'
|
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
|
|
31
31
|
spec.add_development_dependency 'bundler', '~> 1.3'
|
32
32
|
spec.add_development_dependency 'aptible-tasks', '>= 0.2.0'
|
33
33
|
spec.add_development_dependency 'rake'
|
34
|
-
spec.add_development_dependency 'rspec', '~> 2
|
34
|
+
spec.add_development_dependency 'rspec', '~> 3.2'
|
35
35
|
spec.add_development_dependency 'pry'
|
36
36
|
spec.add_development_dependency 'climate_control'
|
37
37
|
spec.add_development_dependency 'fabrication', '~> 2.15.2'
|
data/lib/aptible/cli/agent.rb
CHANGED
@@ -21,6 +21,7 @@ require_relative 'subcommands/rebuild'
|
|
21
21
|
require_relative 'subcommands/restart'
|
22
22
|
require_relative 'subcommands/ssh'
|
23
23
|
require_relative 'subcommands/backup'
|
24
|
+
require_relative 'subcommands/operation'
|
24
25
|
|
25
26
|
module Aptible
|
26
27
|
module CLI
|
@@ -28,6 +29,7 @@ module Aptible
|
|
28
29
|
include Thor::Actions
|
29
30
|
|
30
31
|
include Helpers::Token
|
32
|
+
include Helpers::Ssh
|
31
33
|
include Subcommands::Apps
|
32
34
|
include Subcommands::Config
|
33
35
|
include Subcommands::DB
|
@@ -38,6 +40,7 @@ module Aptible
|
|
38
40
|
include Subcommands::Restart
|
39
41
|
include Subcommands::SSH
|
40
42
|
include Subcommands::Backup
|
43
|
+
include Subcommands::Operation
|
41
44
|
|
42
45
|
# Forward return codes on failures.
|
43
46
|
def self.exit_on_failure?
|
@@ -55,12 +55,19 @@ module Aptible
|
|
55
55
|
# Creates a local tunnel and yields the helper
|
56
56
|
|
57
57
|
def with_local_tunnel(database, port = 0)
|
58
|
-
|
59
|
-
ssh_args(database))
|
58
|
+
op = database.create_operation!(type: 'tunnel', status: 'succeeded')
|
60
59
|
|
61
|
-
|
62
|
-
|
63
|
-
|
60
|
+
with_ssh_cmd(op) do |base_ssh_cmd, credential|
|
61
|
+
ssh_cmd = base_ssh_cmd + ['-o', 'SendEnv=ACCESS_TOKEN']
|
62
|
+
ssh_env = { 'ACCESS_TOKEN' => fetch_token }
|
63
|
+
|
64
|
+
socket_path = credential.ssh_port_forward_socket
|
65
|
+
tunnel_helper = Helpers::Tunnel.new(ssh_env, ssh_cmd, socket_path)
|
66
|
+
|
67
|
+
tunnel_helper.start(port)
|
68
|
+
yield tunnel_helper if block_given?
|
69
|
+
tunnel_helper.stop
|
70
|
+
end
|
64
71
|
end
|
65
72
|
|
66
73
|
# Creates a local PG tunnel and yields the url to it
|
@@ -84,20 +91,6 @@ module Aptible
|
|
84
91
|
"#{uri.scheme}://#{uri.user}:#{uri.password}@" \
|
85
92
|
"localhost.aptible.in:#{local_port}#{uri.path}"
|
86
93
|
end
|
87
|
-
|
88
|
-
def ssh_env(database)
|
89
|
-
{
|
90
|
-
'ACCESS_TOKEN' => fetch_token,
|
91
|
-
'APTIBLE_DATABASE' => database.href
|
92
|
-
}
|
93
|
-
end
|
94
|
-
|
95
|
-
def ssh_args(database)
|
96
|
-
broadwayjoe_ssh_command(database.account) + [
|
97
|
-
'-o', 'SendEnv=ACCESS_TOKEN',
|
98
|
-
'-o', 'SendEnv=APTIBLE_DATABASE'
|
99
|
-
]
|
100
|
-
end
|
101
94
|
end
|
102
95
|
end
|
103
96
|
end
|
@@ -11,8 +11,8 @@ module Aptible
|
|
11
11
|
def poll_for_success(operation)
|
12
12
|
wait_for_completion operation
|
13
13
|
return if operation.status == 'succeeded'
|
14
|
-
|
15
|
-
|
14
|
+
|
15
|
+
fail Thor::Error, "Operation ##{operation.id} failed."
|
16
16
|
end
|
17
17
|
|
18
18
|
def wait_for_completion(operation)
|
@@ -23,23 +23,34 @@ module Aptible
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def attach_to_operation_logs(operation)
|
26
|
+
# TODO: This isn't actually guaranteed to connect to the operation
|
27
|
+
# logs, since the action will depend on what operation we're actually
|
28
|
+
# connecting for. There might be ways to make this better.
|
26
29
|
ENV['ACCESS_TOKEN'] = fetch_token
|
27
|
-
ENV['APTIBLE_OPERATION'] = operation.id.to_s
|
28
|
-
ENV['APTIBLE_CLI_COMMAND'] = 'oplog'
|
29
|
-
|
30
|
-
cmd = dumptruck_ssh_command(operation.resource.account) + [
|
31
|
-
'-o', 'SendEnv=ACCESS_TOKEN',
|
32
|
-
'-o', 'SendEnv=APTIBLE_OPERATION',
|
33
|
-
'-o', 'SendEnv=APTIBLE_CLI_COMMAND'
|
34
|
-
]
|
35
30
|
|
36
|
-
success =
|
31
|
+
success = connect_to_ssh_portal(
|
32
|
+
operation,
|
33
|
+
'-o', 'SendEnv=ACCESS_TOKEN'
|
34
|
+
)
|
37
35
|
|
38
|
-
# If
|
36
|
+
# If the portal is down, fall back to polling for success. If the
|
39
37
|
# operation failed, poll_for_success will immediately fall through to
|
40
38
|
# the error message.
|
41
39
|
poll_for_success(operation) unless success
|
42
40
|
end
|
41
|
+
|
42
|
+
def cancel_operation(operation)
|
43
|
+
puts "Cancelling #{prettify_operation(operation)}..."
|
44
|
+
operation.update!(cancelled: true)
|
45
|
+
end
|
46
|
+
|
47
|
+
def prettify_operation(o)
|
48
|
+
bits = [o.status, o.type, "##{o.id}"]
|
49
|
+
if o.resource.respond_to?(:handle)
|
50
|
+
bits.concat ['on', o.resource.handle]
|
51
|
+
end
|
52
|
+
bits.join ' '
|
53
|
+
end
|
43
54
|
end
|
44
55
|
end
|
45
56
|
end
|
@@ -2,23 +2,80 @@ module Aptible
|
|
2
2
|
module CLI
|
3
3
|
module Helpers
|
4
4
|
module Ssh
|
5
|
-
def
|
6
|
-
|
5
|
+
def connect_to_ssh_portal(operation, *extra_ssh_args)
|
6
|
+
# TODO: Should we rescue Interrupt here?
|
7
|
+
with_ssh_cmd(operation) do |base_ssh_cmd|
|
8
|
+
ssh_cmd = base_ssh_cmd + extra_ssh_args
|
9
|
+
Kernel.system(*ssh_cmd)
|
10
|
+
end
|
7
11
|
end
|
8
12
|
|
9
|
-
def
|
10
|
-
|
13
|
+
def with_ssh_cmd(operation)
|
14
|
+
ensure_ssh_dir!
|
15
|
+
ensure_config!
|
16
|
+
ensure_key!
|
17
|
+
|
18
|
+
operation.with_ssh_cmd(private_key_file) do |cmd, connection|
|
19
|
+
yield cmd + common_ssh_args, connection
|
20
|
+
end
|
11
21
|
end
|
12
22
|
|
13
23
|
private
|
14
24
|
|
15
|
-
def
|
25
|
+
def ensure_ssh_dir!
|
26
|
+
FileUtils.mkdir_p(ssh_dir, mode: 0o700)
|
27
|
+
end
|
28
|
+
|
29
|
+
def ensure_config!
|
30
|
+
return if File.exist?(ssh_config_file)
|
31
|
+
File.open(ssh_config_file, 'w', 0o600) { |f| f.write('') }
|
32
|
+
end
|
33
|
+
|
34
|
+
def ensure_key!
|
35
|
+
key_files = [private_key_file, public_key_file]
|
36
|
+
return if key_files.all? { |f| File.exist?(f) }
|
37
|
+
|
38
|
+
# If we're missing *some* files, then we should clean them up.
|
39
|
+
|
40
|
+
# rubocop:disable Lint/HandleExceptions
|
41
|
+
key_files.each do |key_file|
|
42
|
+
begin
|
43
|
+
File.delete(key_file)
|
44
|
+
rescue Errno::ENOENT
|
45
|
+
# We don't care, that's what we want.
|
46
|
+
end
|
47
|
+
end
|
48
|
+
# rubocop:enable Lint/HandleExceptions
|
49
|
+
|
50
|
+
begin
|
51
|
+
cmd = ['ssh-keygen', '-t', 'rsa', '-N', '', '-f', private_key_file]
|
52
|
+
out, status = Open3.capture2e(*cmd)
|
53
|
+
raise "Failed to generate ssh key: #{out}" unless status.success?
|
54
|
+
rescue Errno::ENOENT
|
55
|
+
raise 'ssh-keygen must be installed'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def ssh_dir
|
60
|
+
File.join ENV['HOME'], '.aptible', 'ssh'
|
61
|
+
end
|
62
|
+
|
63
|
+
def ssh_config_file
|
64
|
+
File.join ssh_dir, 'config'
|
65
|
+
end
|
66
|
+
|
67
|
+
def private_key_file
|
68
|
+
File.join ssh_dir, 'id_rsa'
|
69
|
+
end
|
70
|
+
|
71
|
+
def public_key_file
|
72
|
+
"#{private_key_file}.pub"
|
73
|
+
end
|
74
|
+
|
75
|
+
def common_ssh_args
|
16
76
|
log_level = ENV['APTIBLE_SSH_VERBOSE'] ? 'VERBOSE' : 'ERROR'
|
17
77
|
|
18
78
|
[
|
19
|
-
'ssh',
|
20
|
-
"root@#{account.bastion_host}",
|
21
|
-
'-p', account.public_send(port_method).to_s,
|
22
79
|
'-o', 'StrictHostKeyChecking=no',
|
23
80
|
'-o', 'UserKnownHostsFile=/dev/null',
|
24
81
|
'-o', 'TCPKeepAlive=yes',
|
@@ -26,7 +83,8 @@ module Aptible
|
|
26
83
|
'-o', 'ServerAliveInterval=60',
|
27
84
|
'-o', "LogLevel=#{log_level}",
|
28
85
|
'-o', 'ControlMaster=no',
|
29
|
-
'-o', 'ControlPath=none'
|
86
|
+
'-o', 'ControlPath=none',
|
87
|
+
'-F', ssh_config_file
|
30
88
|
]
|
31
89
|
end
|
32
90
|
end
|
@@ -23,40 +23,25 @@ module Aptible
|
|
23
23
|
end
|
24
24
|
|
25
25
|
class Tunnel
|
26
|
-
def initialize(env, ssh_cmd)
|
26
|
+
def initialize(env, ssh_cmd, socket_path)
|
27
27
|
@env = env
|
28
28
|
@ssh_cmd = ssh_cmd
|
29
|
+
@socket_path = socket_path
|
29
30
|
end
|
30
31
|
|
31
32
|
def start(desired_port = 0)
|
32
33
|
@local_port = desired_port
|
33
34
|
@local_port = random_local_port if @local_port.zero?
|
34
35
|
|
35
|
-
# First, grab a remote port
|
36
|
-
out, err, status = Open3.capture3(@env, *@ssh_cmd)
|
37
|
-
fail "Failed to request remote port: #{err}" unless status.success?
|
38
|
-
remote_port = out.chomp
|
39
|
-
|
40
|
-
# Then, spin up a SSH session using that port and port forwarding.
|
41
|
-
# Pass ExitOnForwardFailure to ensure nothing else can be listening
|
42
|
-
# on this port (thanks to Diego Argueta for reporting this issue).
|
43
|
-
tunnel_env = @env.merge(
|
44
|
-
'TUNNEL_PORT' => remote_port, # Request a specific port
|
45
|
-
'TUNNEL_SIGNAL_OPEN' => '1' # Request signal when tunnel is up
|
46
|
-
)
|
47
|
-
|
48
|
-
# TODO: Dynamically compose SendEnv from tunnel_env
|
49
36
|
tunnel_cmd = @ssh_cmd + [
|
50
|
-
'-L', "#{@local_port}
|
51
|
-
'-o', 'SendEnv=TUNNEL_PORT',
|
52
|
-
'-o', 'SendEnv=TUNNEL_SIGNAL_OPEN',
|
37
|
+
'-L', "#{@local_port}:#{@socket_path}",
|
53
38
|
'-o', 'ExitOnForwardFailure=yes'
|
54
39
|
]
|
55
40
|
|
56
41
|
out_read, out_write = IO.pipe
|
57
42
|
err_read, err_write = IO.pipe
|
58
43
|
|
59
|
-
@pid = Process.spawn(
|
44
|
+
@pid = Process.spawn(@env, *tunnel_cmd, SPAWN_OPTS
|
60
45
|
.merge(in: :close, out: out_write, err: err_write))
|
61
46
|
|
62
47
|
# Wait for the tunnel to come up before returning. The other end
|
@@ -8,27 +8,33 @@ module Aptible
|
|
8
8
|
thor.class_eval do
|
9
9
|
include Helpers::Operation
|
10
10
|
include Helpers::App
|
11
|
+
include Helpers::Database
|
11
12
|
|
12
|
-
desc 'logs', 'Follows logs from a running app'
|
13
|
+
desc 'logs', 'Follows logs from a running app or database'
|
13
14
|
app_options
|
15
|
+
option :database
|
14
16
|
def logs
|
15
|
-
app
|
16
|
-
|
17
|
-
fail Thor::Error,
|
18
|
-
"Have you deployed #{app.handle} yet?"
|
17
|
+
if options[:app] && options[:database]
|
18
|
+
m = 'You must specify only one of --app and --database'
|
19
|
+
fail Thor::Error, m
|
19
20
|
end
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
22
|
+
resource = \
|
23
|
+
if options[:database]
|
24
|
+
ensure_database(options.merge(db: options[:database]))
|
25
|
+
else
|
26
|
+
ensure_app(options)
|
27
|
+
end
|
28
|
+
|
29
|
+
unless resource.status == 'provisioned'
|
30
|
+
fail Thor::Error, 'Unable to retrieve logs. ' \
|
31
|
+
"Have you deployed #{resource.handle} yet?"
|
32
|
+
end
|
24
33
|
|
25
|
-
|
26
|
-
'-o', 'SendEnv=ACCESS_TOKEN',
|
27
|
-
'-o', 'SendEnv=APTIBLE_APP',
|
28
|
-
'-o', 'SendEnv=APTIBLE_CLI_COMMAND'
|
29
|
-
]
|
34
|
+
op = resource.create_operation!(type: 'logs', status: 'succeeded')
|
30
35
|
|
31
|
-
|
36
|
+
ENV['ACCESS_TOKEN'] = fetch_token
|
37
|
+
connect_to_ssh_portal(op, '-o', 'SendEnv=ACCESS_TOKEN', '-T')
|
32
38
|
end
|
33
39
|
end
|
34
40
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Aptible
|
2
|
+
module CLI
|
3
|
+
module Subcommands
|
4
|
+
module Operation
|
5
|
+
def self.included(thor)
|
6
|
+
thor.class_eval do
|
7
|
+
include Helpers::Token
|
8
|
+
include Helpers::Operation
|
9
|
+
|
10
|
+
desc 'operation:cancel OPERATION_ID', 'Cancel a running operation'
|
11
|
+
define_method 'operation:cancel' do |operation_id|
|
12
|
+
o = Aptible::Api::Operation.find(operation_id, token: fetch_token)
|
13
|
+
fail "Operation ##{operation_id} not found" if o.nil?
|
14
|
+
|
15
|
+
puts "Requesting cancellation on #{prettify_operation(o)}..."
|
16
|
+
o.update!(cancelled: true)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -12,20 +12,15 @@ module Aptible
|
|
12
12
|
desc 'ps', 'Display running processes for an app - DEPRECATED'
|
13
13
|
app_options
|
14
14
|
def ps
|
15
|
-
app = ensure_app(options)
|
16
15
|
deprecated('This command is deprecated on Aptible v2 stacks.')
|
17
16
|
|
18
|
-
|
19
|
-
ENV['APTIBLE_APP'] = app.href
|
20
|
-
ENV['APTIBLE_CLI_COMMAND'] = 'ps'
|
17
|
+
app = ensure_app(options)
|
21
18
|
|
22
|
-
|
23
|
-
'-o', 'SendEnv=ACCESS_TOKEN',
|
24
|
-
'-o', 'SendEnv=APTIBLE_APP',
|
25
|
-
'-o', 'SendEnv=APTIBLE_CLI_COMMAND'
|
26
|
-
]
|
19
|
+
op = app.create_operation!(type: 'ps', status: 'succeeded')
|
27
20
|
|
28
|
-
|
21
|
+
ENV['ACCESS_TOKEN'] = fetch_token
|
22
|
+
opts = ['-o', 'SendEnv=ACCESS_TOKEN']
|
23
|
+
connect_to_ssh_portal(op, *opts)
|
29
24
|
end
|
30
25
|
end
|
31
26
|
end
|
@@ -8,7 +8,6 @@ module Aptible
|
|
8
8
|
thor.class_eval do
|
9
9
|
include Helpers::Operation
|
10
10
|
include Helpers::App
|
11
|
-
include Helpers::Ssh
|
12
11
|
|
13
12
|
desc 'ssh [COMMAND]', 'Run a command against an app'
|
14
13
|
long_desc <<-LONGDESC
|
@@ -21,18 +20,14 @@ module Aptible
|
|
21
20
|
def ssh(*args)
|
22
21
|
app = ensure_app(options)
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
cmd = broadwayjoe_ssh_command(app.account) + [
|
29
|
-
'-o', 'SendEnv=ACCESS_TOKEN',
|
30
|
-
'-o', 'SendEnv=APTIBLE_APP',
|
31
|
-
'-o', 'SendEnv=APTIBLE_COMMAND'
|
32
|
-
]
|
33
|
-
cmd << '-tt' if options[:force_tty]
|
23
|
+
op = app.create_operation!(type: 'execute',
|
24
|
+
command: command_from_args(*args),
|
25
|
+
status: 'succeeded')
|
34
26
|
|
35
|
-
|
27
|
+
ENV['ACCESS_TOKEN'] = fetch_token
|
28
|
+
opts = ['-o', 'SendEnv=ACCESS_TOKEN']
|
29
|
+
opts << '-tt' if options[:force_tty]
|
30
|
+
connect_to_ssh_portal(op, *opts)
|
36
31
|
end
|
37
32
|
|
38
33
|
private
|
data/lib/aptible/cli/version.rb
CHANGED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Aptible::CLI::Helpers::Operation do
|
4
|
+
subject { Class.new.send(:include, described_class).new }
|
5
|
+
|
6
|
+
describe '#prettify_operation' do
|
7
|
+
it 'works for app operations' do
|
8
|
+
op = Fabricate(:operation, id: 123, type: 'deploy', status: 'running',
|
9
|
+
resource: Fabricate(:app, handle: 'myapp'))
|
10
|
+
|
11
|
+
expect(subject.prettify_operation(op))
|
12
|
+
.to eq('running deploy #123 on myapp')
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'works for backup operations' do
|
16
|
+
op = Fabricate(:operation, id: 123, type: 'restore', status: 'queued',
|
17
|
+
resource: Fabricate(:backup))
|
18
|
+
|
19
|
+
expect(subject.prettify_operation(op))
|
20
|
+
.to eq('queued restore #123')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Aptible::CLI::Helpers::Ssh do
|
4
|
+
let!(:work_dir) { Dir.mktmpdir }
|
5
|
+
after { FileUtils.remove_entry work_dir }
|
6
|
+
around { |example| ClimateControl.modify(HOME: work_dir) { example.run } }
|
7
|
+
|
8
|
+
subject { Class.new.send(:include, described_class).new }
|
9
|
+
|
10
|
+
let(:ssh_dir) { File.join(work_dir, '.aptible', 'ssh') }
|
11
|
+
let(:config_file) { File.join(ssh_dir, 'config') }
|
12
|
+
let(:private_key_file) { File.join(ssh_dir, 'id_rsa') }
|
13
|
+
let(:public_key_file) { "#{private_key_file}.pub" }
|
14
|
+
|
15
|
+
describe '#ensure_ssh_dir!' do
|
16
|
+
it 'creates the directory' do
|
17
|
+
subject.send(:ensure_ssh_dir!)
|
18
|
+
expect(Dir.exist?(ssh_dir)).to be_truthy
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'works if the directory already exists' do
|
22
|
+
subject.send(:ensure_ssh_dir!)
|
23
|
+
subject.send(:ensure_ssh_dir!)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#ensure_config!' do
|
28
|
+
before { subject.send(:ensure_ssh_dir!) }
|
29
|
+
|
30
|
+
it 'creates the config file' do
|
31
|
+
subject.send(:ensure_config!)
|
32
|
+
expect(File.exist?(config_file)).to be_truthy
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#ensure_key!' do
|
37
|
+
before { subject.send(:ensure_ssh_dir!) }
|
38
|
+
|
39
|
+
it 'creates the key if it does not exist' do
|
40
|
+
subject.send(:ensure_key!)
|
41
|
+
|
42
|
+
expect(File.exist?(private_key_file)).to be_truthy
|
43
|
+
expect(File.exist?(public_key_file)).to be_truthy
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'does not recreate the key if it already exists' do
|
47
|
+
subject.send(:ensure_key!)
|
48
|
+
k1 = File.read(private_key_file)
|
49
|
+
subject.send(:ensure_key!)
|
50
|
+
k2 = File.read(private_key_file)
|
51
|
+
|
52
|
+
expect(k2).to eq(k1)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'recreates the key if either part is missing' do
|
56
|
+
subject.send(:ensure_key!)
|
57
|
+
k1 = File.read(private_key_file)
|
58
|
+
File.delete(private_key_file)
|
59
|
+
|
60
|
+
subject.send(:ensure_key!)
|
61
|
+
k2 = File.read(private_key_file)
|
62
|
+
File.delete(public_key_file)
|
63
|
+
|
64
|
+
subject.send(:ensure_key!)
|
65
|
+
k3 = File.read(private_key_file)
|
66
|
+
|
67
|
+
expect(k2).not_to eq(k1)
|
68
|
+
expect(k3).not_to eq(k2)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#with_ssh_cmd' do
|
73
|
+
it 'delegates and yields usable SSH parameters' do
|
74
|
+
operation = double('operation')
|
75
|
+
connection = double('connection')
|
76
|
+
|
77
|
+
expect(operation).to receive(:with_ssh_cmd).with(private_key_file)
|
78
|
+
.and_yield(['some-ssh'], connection)
|
79
|
+
|
80
|
+
has_yielded = false
|
81
|
+
|
82
|
+
subject.with_ssh_cmd(operation) do |cmd, c|
|
83
|
+
expect(cmd).to include('some-ssh')
|
84
|
+
expect(cmd).to include(config_file)
|
85
|
+
expect(c).to be(connection)
|
86
|
+
has_yielded = true
|
87
|
+
end
|
88
|
+
|
89
|
+
expect(has_yielded).to be_truthy
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -3,56 +3,48 @@ require 'spec_helper'
|
|
3
3
|
describe Aptible::CLI::Helpers::Tunnel do
|
4
4
|
include_context 'mock ssh'
|
5
5
|
|
6
|
-
it '
|
7
|
-
helper = described_class.new({}, ['ssh'])
|
6
|
+
it 'opens a tunnel' do
|
7
|
+
helper = described_class.new({}, ['ssh'], '/some.sock')
|
8
8
|
|
9
9
|
helper.start(0)
|
10
10
|
helper.stop
|
11
11
|
|
12
12
|
mock_argv = read_mock_argv
|
13
|
-
expect(mock_argv.size).to eq(
|
13
|
+
expect(mock_argv.size).to eq(4)
|
14
14
|
|
15
15
|
expect(mock_argv.shift).to eq('-L')
|
16
|
-
expect(mock_argv.shift).to match(
|
17
|
-
expect(mock_argv.shift).to eq('-o')
|
18
|
-
expect(mock_argv.shift).to eq('SendEnv=TUNNEL_PORT')
|
19
|
-
expect(mock_argv.shift).to eq('-o')
|
20
|
-
expect(mock_argv.shift).to eq('SendEnv=TUNNEL_SIGNAL_OPEN')
|
16
|
+
expect(mock_argv.shift).to match(%r{\d+:/some\.sock$})
|
21
17
|
expect(mock_argv.shift).to eq('-o')
|
22
18
|
expect(mock_argv.shift).to eq('ExitOnForwardFailure=yes')
|
23
19
|
end
|
24
20
|
|
25
21
|
it 'accepts a desired local port' do
|
26
|
-
helper = described_class.new({}, ['ssh'])
|
22
|
+
helper = described_class.new({}, ['ssh'], '/some.sock')
|
27
23
|
helper.start(5678)
|
28
24
|
helper.stop
|
29
25
|
|
30
26
|
mock_argv = read_mock_argv
|
31
|
-
expect(mock_argv.size).to eq(
|
27
|
+
expect(mock_argv.size).to eq(4)
|
32
28
|
|
33
29
|
expect(mock_argv.shift).to eq('-L')
|
34
|
-
expect(mock_argv.shift).to eq('5678
|
35
|
-
end
|
36
|
-
|
37
|
-
it 'captures and displays port discovery errors' do
|
38
|
-
helper = described_class.new({ 'FAIL_PORT' => '1' }, ['ssh'])
|
39
|
-
expect { helper.start }
|
40
|
-
.to raise_error(/Failed to request.*Something went wrong/m)
|
30
|
+
expect(mock_argv.shift).to eq('5678:/some.sock')
|
41
31
|
end
|
42
32
|
|
43
33
|
it 'captures and displays tunnel errors' do
|
44
|
-
helper = described_class.new({ 'FAIL_TUNNEL' => '1' }, ['ssh']
|
34
|
+
helper = described_class.new({ 'FAIL_TUNNEL' => '1' }, ['ssh'],
|
35
|
+
'/some.sock')
|
36
|
+
|
45
37
|
expect { helper.start(0) }
|
46
38
|
.to raise_error(/Tunnel did not come up.*Something went wrong/m)
|
47
39
|
end
|
48
40
|
|
49
41
|
it 'should fail if #port is called before #start' do
|
50
|
-
socat = described_class.new({}, [])
|
42
|
+
socat = described_class.new({}, [], '/some.sock')
|
51
43
|
expect { socat.port }.to raise_error(/You must call #start/)
|
52
44
|
end
|
53
45
|
|
54
46
|
it 'should fail if #stop is called before #start' do
|
55
|
-
socat = described_class.new({}, [])
|
47
|
+
socat = described_class.new({}, [], '/some.sock')
|
56
48
|
expect { socat.stop }.to raise_error(/You must call #start/)
|
57
49
|
end
|
58
50
|
end
|
@@ -3,26 +3,59 @@ require 'spec_helper'
|
|
3
3
|
describe Aptible::CLI::Agent do
|
4
4
|
before { subject.stub(:ask) }
|
5
5
|
before { subject.stub(:save_token) }
|
6
|
-
before { subject.stub(:fetch_token) {
|
6
|
+
before { subject.stub(:fetch_token) { 'some token' } }
|
7
7
|
|
8
|
-
let
|
9
|
-
let
|
8
|
+
let(:app) { Fabricate(:app, handle: 'foo') }
|
9
|
+
let(:database) { Fabricate(:database, handle: 'bar', status: 'provisioned') }
|
10
|
+
let(:service) { Fabricate(:service, app: app) }
|
10
11
|
|
11
12
|
describe '#logs' do
|
12
13
|
before { allow(Aptible::Api::Account).to receive(:all) { [app.account] } }
|
13
|
-
before { allow(Aptible::Api::App).to receive(:all) { [app] } }
|
14
|
-
before { subject.options = { app: app.handle } }
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
context 'App resource' do
|
16
|
+
before { allow(Aptible::Api::App).to receive(:all) { [app] } }
|
17
|
+
before { subject.options = { app: app.handle } }
|
18
|
+
|
19
|
+
it 'should fail if the app is unprovisioned' do
|
20
|
+
app.status = 'pending'
|
21
|
+
expect { subject.send('logs') }
|
22
|
+
.to raise_error(Thor::Error, /Have you deployed foo yet/)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'create a logs operation and connect to the SSH portal' do
|
26
|
+
op = double('operation')
|
27
|
+
expect(app).to receive(:create_operation!).with(
|
28
|
+
type: 'logs', status: 'succeeded'
|
29
|
+
).and_return(op)
|
30
|
+
expect(subject).to receive(:connect_to_ssh_portal).with(op, any_args)
|
31
|
+
subject.send('logs')
|
32
|
+
end
|
20
33
|
end
|
21
34
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
35
|
+
context 'Database resource' do
|
36
|
+
before { allow(Aptible::Api::Database).to receive(:all) { [database] } }
|
37
|
+
before { subject.options = { database: database.handle } }
|
38
|
+
|
39
|
+
it 'should fail if the database is unprovisioned' do
|
40
|
+
database.status = 'pending'
|
41
|
+
expect { subject.send('logs') }
|
42
|
+
.to raise_error(Thor::Error, /Have you deployed bar yet/)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'create a logs operation and connect to the SSH portal' do
|
46
|
+
op = double('operation')
|
47
|
+
expect(database).to receive(:create_operation!).with(
|
48
|
+
type: 'logs', status: 'succeeded'
|
49
|
+
).and_return(op)
|
50
|
+
expect(subject).to receive(:connect_to_ssh_portal).with(op, any_args)
|
51
|
+
subject.send('logs')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should fail when passed both --app and --database' do
|
56
|
+
subject.options = { app: 'foo', database: 'bar' }
|
57
|
+
|
58
|
+
expect { subject.send(:logs) }.to raise_error(/only one of/im)
|
26
59
|
end
|
27
60
|
end
|
28
61
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Aptible::CLI::Agent do
|
4
|
+
let(:token) { 'some-token' }
|
5
|
+
let(:operation) { Fabricate(:operation) }
|
6
|
+
|
7
|
+
before do
|
8
|
+
allow(subject).to receive(:fetch_token).and_return(token)
|
9
|
+
allow(subject).to receive(:say) { |m| messages << m }
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#operation:cancel' do
|
13
|
+
it 'fails if the operation cannot be found' do
|
14
|
+
expect(Aptible::Api::Operation).to receive(:find).with(1, token: token)
|
15
|
+
.and_return(nil)
|
16
|
+
|
17
|
+
expect { subject.send('operation:cancel', 1) }
|
18
|
+
.to raise_error('Operation #1 not found')
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'sets the cancelled flag on the operation' do
|
22
|
+
expect(Aptible::Api::Operation).to receive(:find).with(1, token: token)
|
23
|
+
.and_return(operation)
|
24
|
+
|
25
|
+
expect(operation).to receive(:update!).with(cancelled: true)
|
26
|
+
|
27
|
+
subject.send('operation:cancel', 1)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/spec/mock/ssh
CHANGED
@@ -1,18 +1,9 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
# Emulate server behavior
|
4
|
-
|
5
|
-
if ENV['TUNNEL_PORT']
|
6
|
-
fail 'Something went wrong!' if ENV['FAIL_TUNNEL']
|
7
|
-
puts 'TUNNEL READY'
|
8
|
-
else
|
9
|
-
fail 'Something went wrong!' if ENV['FAIL_PORT']
|
10
|
-
puts 1234
|
11
|
-
end
|
12
|
-
|
13
|
-
# Log to SSH_MOCK_OUTFILE
|
14
2
|
require 'json'
|
15
3
|
|
4
|
+
fail 'Something went wrong!' if ENV['FAIL_TUNNEL']
|
5
|
+
|
6
|
+
# Log arguments to SSH_MOCK_OUTFILE
|
16
7
|
File.open(ENV.fetch('SSH_MOCK_OUTFILE'), 'w') do |f|
|
17
8
|
f.write({
|
18
9
|
'argc' => ARGV.size,
|
@@ -20,3 +11,5 @@ File.open(ENV.fetch('SSH_MOCK_OUTFILE'), 'w') do |f|
|
|
20
11
|
'env' => ENV.to_hash
|
21
12
|
}.to_json)
|
22
13
|
end
|
14
|
+
|
15
|
+
puts 'TUNNEL READY'
|
data/spec/mock/ssh_mock.rb
CHANGED
@@ -1,18 +1,9 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
# Emulate server behavior
|
4
|
-
|
5
|
-
if ENV['TUNNEL_PORT']
|
6
|
-
fail 'Something went wrong!' if ENV['FAIL_TUNNEL']
|
7
|
-
puts 'TUNNEL READY'
|
8
|
-
else
|
9
|
-
fail 'Something went wrong!' if ENV['FAIL_PORT']
|
10
|
-
puts 1234
|
11
|
-
end
|
12
|
-
|
13
|
-
# Log to SSH_MOCK_OUTFILE
|
14
2
|
require 'json'
|
15
3
|
|
4
|
+
fail 'Something went wrong!' if ENV['FAIL_TUNNEL']
|
5
|
+
|
6
|
+
# Log arguments to SSH_MOCK_OUTFILE
|
16
7
|
File.open(ENV.fetch('SSH_MOCK_OUTFILE'), 'w') do |f|
|
17
8
|
f.write({
|
18
9
|
'argc' => ARGV.size,
|
@@ -20,3 +11,5 @@ File.open(ENV.fetch('SSH_MOCK_OUTFILE'), 'w') do |f|
|
|
20
11
|
'env' => ENV.to_hash
|
21
12
|
}.to_json)
|
22
13
|
end
|
14
|
+
|
15
|
+
puts 'TUNNEL READY'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aptible-cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Frank Macreery
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.9.
|
19
|
+
version: 0.9.14
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.9.
|
26
|
+
version: 0.9.14
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: aptible-auth
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -156,14 +156,14 @@ dependencies:
|
|
156
156
|
requirements:
|
157
157
|
- - "~>"
|
158
158
|
- !ruby/object:Gem::Version
|
159
|
-
version: '2
|
159
|
+
version: '3.2'
|
160
160
|
type: :development
|
161
161
|
prerelease: false
|
162
162
|
version_requirements: !ruby/object:Gem::Requirement
|
163
163
|
requirements:
|
164
164
|
- - "~>"
|
165
165
|
- !ruby/object:Gem::Version
|
166
|
-
version: '2
|
166
|
+
version: '3.2'
|
167
167
|
- !ruby/object:Gem::Dependency
|
168
168
|
name: pry
|
169
169
|
requirement: !ruby/object:Gem::Requirement
|
@@ -241,6 +241,7 @@ files:
|
|
241
241
|
- lib/aptible/cli/subcommands/db.rb
|
242
242
|
- lib/aptible/cli/subcommands/domains.rb
|
243
243
|
- lib/aptible/cli/subcommands/logs.rb
|
244
|
+
- lib/aptible/cli/subcommands/operation.rb
|
244
245
|
- lib/aptible/cli/subcommands/ps.rb
|
245
246
|
- lib/aptible/cli/subcommands/rebuild.rb
|
246
247
|
- lib/aptible/cli/subcommands/restart.rb
|
@@ -249,14 +250,16 @@ files:
|
|
249
250
|
- spec/aptible/cli/agent_spec.rb
|
250
251
|
- spec/aptible/cli/helpers/git_remote_handle_strategy_spec.rb
|
251
252
|
- spec/aptible/cli/helpers/handle_from_git_remote_spec.rb
|
253
|
+
- spec/aptible/cli/helpers/operation_spec.rb
|
252
254
|
- spec/aptible/cli/helpers/options_handle_strategy_spec.rb
|
255
|
+
- spec/aptible/cli/helpers/ssh_spec.rb
|
253
256
|
- spec/aptible/cli/helpers/tunnel_spec.rb
|
254
257
|
- spec/aptible/cli/subcommands/apps_spec.rb
|
255
258
|
- spec/aptible/cli/subcommands/backup_spec.rb
|
256
259
|
- spec/aptible/cli/subcommands/db_spec.rb
|
257
260
|
- spec/aptible/cli/subcommands/domains_spec.rb
|
258
261
|
- spec/aptible/cli/subcommands/logs_spec.rb
|
259
|
-
- spec/aptible/cli/subcommands/
|
262
|
+
- spec/aptible/cli/subcommands/operation_spec.rb
|
260
263
|
- spec/aptible/cli/subcommands/restart_spec.rb
|
261
264
|
- spec/fabricators/account_fabricator.rb
|
262
265
|
- spec/fabricators/app_fabricator.rb
|
@@ -299,14 +302,16 @@ test_files:
|
|
299
302
|
- spec/aptible/cli/agent_spec.rb
|
300
303
|
- spec/aptible/cli/helpers/git_remote_handle_strategy_spec.rb
|
301
304
|
- spec/aptible/cli/helpers/handle_from_git_remote_spec.rb
|
305
|
+
- spec/aptible/cli/helpers/operation_spec.rb
|
302
306
|
- spec/aptible/cli/helpers/options_handle_strategy_spec.rb
|
307
|
+
- spec/aptible/cli/helpers/ssh_spec.rb
|
303
308
|
- spec/aptible/cli/helpers/tunnel_spec.rb
|
304
309
|
- spec/aptible/cli/subcommands/apps_spec.rb
|
305
310
|
- spec/aptible/cli/subcommands/backup_spec.rb
|
306
311
|
- spec/aptible/cli/subcommands/db_spec.rb
|
307
312
|
- spec/aptible/cli/subcommands/domains_spec.rb
|
308
313
|
- spec/aptible/cli/subcommands/logs_spec.rb
|
309
|
-
- spec/aptible/cli/subcommands/
|
314
|
+
- spec/aptible/cli/subcommands/operation_spec.rb
|
310
315
|
- spec/aptible/cli/subcommands/restart_spec.rb
|
311
316
|
- spec/fabricators/account_fabricator.rb
|
312
317
|
- spec/fabricators/app_fabricator.rb
|
@@ -1,36 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe Aptible::CLI::Agent do
|
4
|
-
include_context 'mock ssh'
|
5
|
-
|
6
|
-
let(:account) do
|
7
|
-
Fabricate(:account, bastion_host: 'bastion.com', dumptruck_port: 45022)
|
8
|
-
end
|
9
|
-
let(:app) { Fabricate(:app, account: account) }
|
10
|
-
|
11
|
-
before { subject.stub(:ask) }
|
12
|
-
before { subject.stub(:save_token) }
|
13
|
-
before { subject.stub(:fetch_token) { double 'token' } }
|
14
|
-
before { subject.stub(:ensure_app) { app } }
|
15
|
-
|
16
|
-
before do
|
17
|
-
allow(Kernel).to receive(:exec) do |*args|
|
18
|
-
Kernel.system(*args)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
describe '#ps' do
|
23
|
-
it 'should set ENV["APTIBLE_CLI_COMMAND"]' do
|
24
|
-
subject.send('ps')
|
25
|
-
expect(read_mock_env['APTIBLE_CLI_COMMAND']).to eq('ps')
|
26
|
-
end
|
27
|
-
|
28
|
-
it 'should construct a proper SSH call' do
|
29
|
-
subject.send('ps')
|
30
|
-
|
31
|
-
mock_argv = read_mock_argv
|
32
|
-
expect(mock_argv).to include('root@bastion.com')
|
33
|
-
expect(mock_argv).to include('45022')
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|