aptible-cli 0.7.5 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|