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