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