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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a46c716db34062c50939791b35288542ab1dacc4
4
- data.tar.gz: 54293b621aba536d05c4ea25740fe61530e9209f
3
+ metadata.gz: fe2b01937188ac5154d4b1f3ad75c4b614099f6b
4
+ data.tar.gz: 34b46d94e014464efab70894071fbe79e0db388d
5
5
  SHA512:
6
- metadata.gz: 35fadaf8062518242be514c26c66f406cb9f3ebf79128986852a4a51ef84365610f5058b90fbdcf426a18e5bfc68d4ec3afd03935b6e08d9955961bc1f24d0bb
7
- data.tar.gz: 96a7acb0ac08381a76753035670d6e26f3d72347ce1b5c2f64f7259a960b73ad12de3e35aedd0b30890a7d2701785059f45218a107fc35a6626106875aafe367
6
+ metadata.gz: 621652b7947a133f0c5a3981238f1537eba48b18db5f45dbbd26ad492c6480636b76086b7ceb7541df789469770aef204fcf98c996b757d6c1aa63e74a377193
7
+ data.tar.gz: f7497688bb7fa6a4c9c612b8414d2afe040fb1a66e350be1042f6e135728b6120a0831c0b14e00bea9b38325006f2c1d2c060a3ffae3d0129bb6537f41e26d28
@@ -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.7'
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.0'
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'
@@ -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
- tunnel_helper = Helpers::Tunnel.new(ssh_env(database),
59
- ssh_args(database))
58
+ op = database.create_operation!(type: 'tunnel', status: 'succeeded')
60
59
 
61
- tunnel_helper.start(port)
62
- yield tunnel_helper if block_given?
63
- tunnel_helper.stop
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
- fail Thor::Error,
15
- 'Operation failed. Please contact support@aptible.com'
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 = Kernel.system(*cmd)
31
+ success = connect_to_ssh_portal(
32
+ operation,
33
+ '-o', 'SendEnv=ACCESS_TOKEN'
34
+ )
37
35
 
38
- # If Dumptruck is down, fall back to polling for success. If the
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 dumptruck_ssh_command(account)
6
- base_ssh_command(account, :dumptruck_port)
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 broadwayjoe_ssh_command(account)
10
- base_ssh_command(account, :bastion_port)
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 base_ssh_command(account, port_method)
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}:localhost:#{remote_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(tunnel_env, *tunnel_cmd, SPAWN_OPTS
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 = ensure_app(options)
16
- unless app.status == 'provisioned' && app.services.any?
17
- fail Thor::Error, 'Unable to retrieve logs. ' \
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
- ENV['ACCESS_TOKEN'] = fetch_token
22
- ENV['APTIBLE_APP'] = app.href
23
- ENV['APTIBLE_CLI_COMMAND'] = 'logs'
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
- cmd = dumptruck_ssh_command(app.account) + [
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
- Kernel.exec(*cmd)
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
- ENV['ACCESS_TOKEN'] = fetch_token
19
- ENV['APTIBLE_APP'] = app.href
20
- ENV['APTIBLE_CLI_COMMAND'] = 'ps'
17
+ app = ensure_app(options)
21
18
 
22
- cmd = dumptruck_ssh_command(app.account) + [
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
- Kernel.exec(*cmd)
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
- ENV['ACCESS_TOKEN'] = fetch_token
25
- ENV['APTIBLE_APP'] = app.href
26
- ENV['APTIBLE_COMMAND'] = command_from_args(*args)
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
- Kernel.exec(*cmd)
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
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module CLI
3
- VERSION = '0.7.5'.freeze
3
+ VERSION = '0.8.0'.freeze
4
4
  end
5
5
  end
@@ -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 'forwards traffic to the remote port given by the server (1234)' do
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(8)
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(/\d+:localhost:1234$/)
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(8)
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:localhost:1234')
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) { double 'token' } }
6
+ before { subject.stub(:fetch_token) { 'some token' } }
7
7
 
8
- let!(:app) { Fabricate(:app, handle: 'foobar') }
9
- let!(:service) { Fabricate(:service, app: app) }
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
- it 'should fail if the app is unprovisioned' do
17
- app.status = 'pending'
18
- expect { subject.send('logs') }
19
- .to raise_error(Thor::Error, /Have you deployed foobar yet/)
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
- it 'should fail if the app has no services' do
23
- app.services = []
24
- expect { subject.send('logs') }
25
- .to raise_error(Thor::Error, /Have you deployed foobar yet/)
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
@@ -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'
@@ -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.7.5
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.7
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.7
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.0'
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.0'
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/ps_spec.rb
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/ps_spec.rb
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