aptible-cli 0.10.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5d0afaea6319a0ba8278d83dc1de979778dd5842
4
- data.tar.gz: 6d087bb9b0de9c1da3855e4ea85c38a18f48820d
3
+ metadata.gz: 4caf3814e0cffe6799a62625c5d9fb9f79dbd016
4
+ data.tar.gz: dbe1d0e306d35ce276c4ac0d323d0e1ea706aba4
5
5
  SHA512:
6
- metadata.gz: 12d928e9a1fc0266a38e81052132759873220f6ecdc60d4c93a3769a651a39b40ba350efc5140365da88b2ccc25175d63b73c0c7f8d1d0797f8a56128247a48f
7
- data.tar.gz: 634e77e72b255e67b2ecff6df9391d16b2d06052ecfc92f582247e650ee8e81d457a2de55d92635f04f130dc287b241472e1be4e1c0bcc4554346b9e5ece1456
6
+ metadata.gz: 1149e64d3194970dd488a36519eb07d1783d9ef860b587bd04f844de452afb848980ee4340e2e6a2cef0a10afa148812e48fdce7d9f18c296d072e52da5d45f0
7
+ data.tar.gz: 7aed71d453418b1d887d214974a6e60903338154c866d1a5ec90e59ab185f0ad02d0e06bc929046bc1290234572c3785c60fa5ac50c91498f9c6513f628a0127
@@ -5,3 +5,8 @@ rvm:
5
5
  - 2.1.0
6
6
  - 2.2.0
7
7
  - 2.3.0
8
+
9
+ script:
10
+ - bundle exec rake
11
+ - bundle exec script/sync-readme-usage README.md
12
+ - git diff --exit-code
data/README.md CHANGED
@@ -26,39 +26,43 @@ And then run `bundle install`.
26
26
 
27
27
  From `aptible help`:
28
28
 
29
+ <!-- BEGIN USAGE -->
29
30
  ```
30
31
  Commands:
31
- aptible apps # List all applications
32
- aptible apps:create HANDLE # Create a new application
33
- aptible apps:deprovision # Deprovision an app
34
- aptible apps:scale SERVICE [--container-count COUNT] [--container-size SIZE] # Scale a service
35
- aptible backup:list DB_HANDLE # List backups for a database
36
- aptible backup:restore [--handle HANDLE] [--size SIZE_GB] # Restore a backup
37
- aptible config # Print an app's current configuration
38
- aptible config:add # Add an ENV variable to an app
39
- aptible config:rm # Remove an ENV variable from an app
40
- aptible config:set # Alias for config:add
41
- aptible config:unset # Alias for config:rm
42
- aptible db:backup HANDLE # Backup a database
43
- aptible db:clone SOURCE DEST # Clone a database to create a new one
44
- aptible db:create HANDLE # Create a new database
45
- aptible db:deprovision HANDLE # Deprovision a database
46
- aptible db:dump HANDLE # Dump a remote database to file
47
- aptible db:execute HANDLE SQL_FILE # Executes sql against a database
48
- aptible db:list # List all databases
49
- aptible db:reload HANDLE # Reload a database
50
- aptible db:tunnel HANDLE # Create a local tunnel to a database
51
- aptible domains # Print an app's current virtual domains
52
- aptible help [COMMAND] # Describe available commands or one specific command
53
- aptible login # Log in to Aptible
54
- aptible logs # Follows logs from a running app or database
55
- aptible operation:cancel OPERATION_ID # Cancel a running operation
56
- aptible ps # Display running processes for an app - DEPRECATED
57
- aptible rebuild # Rebuild an app, and restart its services
58
- aptible restart # Restart all services associated with an app
59
- aptible ssh [COMMAND] # Run a command against an app
60
- aptible version # Print Aptible CLI version
32
+ aptible apps # List all applications
33
+ aptible apps:create HANDLE # Create a new application
34
+ aptible apps:deprovision # Deprovision an app
35
+ aptible apps:scale SERVICE [--container-count COUNT] [--container-size SIZE_MB] # Scale a service
36
+ aptible backup:list DB_HANDLE # List backups for a database
37
+ aptible backup:restore BACKUP_ID [--handle HANDLE] [--container-size SIZE_MB] [--size SIZE_GB] # Restore a backup
38
+ aptible config # Print an app's current configuration
39
+ aptible config:add # Add an ENV variable to an app
40
+ aptible config:rm # Remove an ENV variable from an app
41
+ aptible config:set # Alias for config:add
42
+ aptible config:unset # Alias for config:rm
43
+ aptible db:backup HANDLE # Backup a database
44
+ aptible db:clone SOURCE DEST # Clone a database to create a new one
45
+ aptible db:create HANDLE[--type TYPE] [--container-size SIZE_MB] [--size SIZE_GB] # Create a new database
46
+ aptible db:deprovision HANDLE # Deprovision a database
47
+ aptible db:dump HANDLE # Dump a remote database to file
48
+ aptible db:execute HANDLE SQL_FILE # Executes sql against a database
49
+ aptible db:list # List all databases
50
+ aptible db:reload HANDLE # Reload a database
51
+ aptible db:restart HANDLE [--container-size SIZE_MB] [--size SIZE_GB] # Restart a database
52
+ aptible db:tunnel HANDLE # Create a local tunnel to a database
53
+ aptible deploy [OPTIONS] [VAR1=VAL1] [VAR=VAL2] ... # Deploy an app
54
+ aptible domains # Print an app's current virtual domains
55
+ aptible help [COMMAND] # Describe available commands or one specific command
56
+ aptible login # Log in to Aptible
57
+ aptible logs # Follows logs from a running app or database
58
+ aptible operation:cancel OPERATION_ID # Cancel a running operation
59
+ aptible ps # Display running processes for an app - DEPRECATED
60
+ aptible rebuild # Rebuild an app, and restart its services
61
+ aptible restart # Restart all services associated with an app
62
+ aptible ssh [COMMAND] # Run a command against an app
63
+ aptible version # Print Aptible CLI version
61
64
  ```
65
+ <!-- END USAGE -->
62
66
 
63
67
  ## Contributing
64
68
 
@@ -20,9 +20,9 @@ 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.20'
24
- spec.add_dependency 'aptible-auth', '~> 0.11.12'
25
- spec.add_dependency 'aptible-resource', '~> 0.3.6'
23
+ spec.add_dependency 'aptible-resource', '~> 0.4.0'
24
+ spec.add_dependency 'aptible-api', '~> 0.9.27'
25
+ spec.add_dependency 'aptible-auth', '~> 0.11.13'
26
26
  spec.add_dependency 'thor', '~> 0.19.1'
27
27
  spec.add_dependency 'git'
28
28
  spec.add_dependency 'term-ansicolor'
@@ -20,6 +20,7 @@ require_relative 'subcommands/domains'
20
20
  require_relative 'subcommands/logs'
21
21
  require_relative 'subcommands/ps'
22
22
  require_relative 'subcommands/rebuild'
23
+ require_relative 'subcommands/deploy'
23
24
  require_relative 'subcommands/restart'
24
25
  require_relative 'subcommands/ssh'
25
26
  require_relative 'subcommands/backup'
@@ -40,6 +41,7 @@ module Aptible
40
41
  include Subcommands::Logs
41
42
  include Subcommands::Ps
42
43
  include Subcommands::Rebuild
44
+ include Subcommands::Deploy
43
45
  include Subcommands::Restart
44
46
  include Subcommands::SSH
45
47
  include Subcommands::Backup
@@ -140,6 +140,21 @@ module Aptible
140
140
  end.select { |a| a.handle == handle }
141
141
  end
142
142
 
143
+ def extract_env(args)
144
+ Hash[args.map do |arg|
145
+ k, v = arg.split('=', 2)
146
+ validate_env_key!(k)
147
+ [k, v]
148
+ end]
149
+ end
150
+
151
+ def validate_env_key!(k)
152
+ # Keys that start with '-' are likely to be mispelled options. As of
153
+ # May 2017 (> 3 years of Aptible!), there are only 2 such cases, both
154
+ # of which are indeed mispelled options.
155
+ raise Thor::Error, "Invalid argument: #{k}" if k.start_with?('-')
156
+ end
157
+
143
158
  private
144
159
 
145
160
  def handle_strategies
@@ -28,7 +28,7 @@ module Aptible
28
28
  # connecting for. There might be ways to make this better.
29
29
  ENV['ACCESS_TOKEN'] = fetch_token
30
30
 
31
- success = connect_to_ssh_portal(
31
+ code = connect_to_ssh_portal(
32
32
  operation,
33
33
  '-o', 'SendEnv=ACCESS_TOKEN'
34
34
  )
@@ -36,7 +36,10 @@ module Aptible
36
36
  # If the portal is down, fall back to polling for success. If the
37
37
  # operation failed, poll_for_success will immediately fall through to
38
38
  # the error message.
39
- poll_for_success(operation) unless success
39
+ unless code == 0
40
+ puts 'Disconnected from logs, waiting for operation to complete'
41
+ poll_for_success(operation)
42
+ end
40
43
  end
41
44
 
42
45
  def cancel_operation(operation)
@@ -3,33 +3,50 @@ module Aptible
3
3
  module Helpers
4
4
  module Ssh
5
5
  def connect_to_ssh_portal(operation, *extra_ssh_args)
6
+ # NOTE: This is a little tricky to get rigt, so before you make any
7
+ # changes, read this.
8
+ #
9
+ # - The first gotcha is that we cannot use Kernel.exec here, because
10
+ # we need to perform cleanup when exiting from
11
+ # operation#with_ssh_cmd.
12
+ #
13
+ # - The second gotcha is that we need to somehow capture the exit
14
+ # status, so that CLI commands that call the SSH portal can proxy
15
+ # this back to their own caller (the most important one here is
16
+ # aptible ssh).
17
+ #
18
+ # To do this, we have to handle interrutps as a signal, as opposed to
19
+ # handle an Interrupt exception. The reason for this has to do with
20
+ # how Ruby's wait is implemented (this happens in process.c's
21
+ # rb_waitpid). There are two main considerations here:
22
+ #
23
+ # - It automatically resumes when it receives EINTR, so our control
24
+ # is pretty high-level here.
25
+ # - It handles interrupts prior to setting $? (this appears to have
26
+ # changed between Ruby 2.2 and 2.3, perhaps the newer implementation
27
+ # behaves differently).
28
+ #
29
+ # Unfortunately, this means that if we receive SIGINT while in
30
+ # Process::wait2, then we never get access to SSH's exitstatus: Ruby
31
+ # throws a Interrupt so we don't have a return value, and it doesn't
32
+ # set $?, so we can't read it back there.
33
+ #
34
+ # Of course, we can't just call Proces::wait2 again, because at this
35
+ # point, we've reaped our child.
36
+ #
37
+ # To solve this, we add our own signal handler on SIGINT, which
38
+ # simply proxies SIGINT to SSH if we happen to have a different
39
+ # process group (which shouldn't be the case), just to be safe and
40
+ # let users exit the CLI.
6
41
  with_ssh_cmd(operation) do |base_ssh_cmd|
7
- ssh_cmd = base_ssh_cmd + extra_ssh_args
8
- begin
9
- Kernel.system(*ssh_cmd)
10
- rescue Interrupt
11
- # Assuming we have a TTY, there are two cases here. Either SSH
12
- # itself has a TTY, in which case it is controlling the TTY and
13
- # the CLI won't be receiving SIGINT when CTRL+C is pressed, or
14
- # SSH has no TTY, in which case the CLI and SSH are sharing the
15
- # same process group, and will both receive SIGINT when CTRL+C
16
- # is pressed and exit accordingly.
17
- #
18
- # I'm not sure how this *should* work on Windows, but it appears
19
- # to work pretty much the same way, except that we'll get an ugly
20
- # "Terminate batch job (Y/N)?" prompt in the no-TTY-for-SSH case,
21
- # which we're likely to have a hard time handling.
22
- #
23
- # Note that this DOES NOT handle the case where the CLI is sent
24
- # SIGINT by another process (as opposed to the line discipline).
25
- # In this case, SSH will continue running in the background. This
26
- # is something we should fix, but for now this 'simple' fix is
27
- # enough to addresses the ugly stack trace we show when
28
- # CTRL+C'ing out of logs.
29
- end
42
+ spawn_passthrough(base_ssh_cmd + extra_ssh_args)
30
43
  end
31
44
  end
32
45
 
46
+ def exit_with_ssh_portal(*args)
47
+ exit connect_to_ssh_portal(*args)
48
+ end
49
+
33
50
  def with_ssh_cmd(operation)
34
51
  ensure_ssh_dir!
35
52
  ensure_config!
@@ -42,6 +59,40 @@ module Aptible
42
59
 
43
60
  private
44
61
 
62
+ def spawn_passthrough(command)
63
+ redirection = { in: :in, out: :out, err: :err, close_others: true }
64
+ pid = Process.spawn(*command, redirection)
65
+
66
+ reset = Signal.trap('SIGINT') do
67
+ # FIXME: If we're on Windows, we don't really know whether SSH
68
+ # received SIGINT or not, so for now, we just ignore it.
69
+ next if Gem.win_platform?
70
+
71
+ begin
72
+ # SSH should be running in our process group, which means that
73
+ # if the user sends CTRL+C, we'll both receive it. In this
74
+ # case, just ignore the signal and let SSH handle it.
75
+ next if Process.getpgid(Process.pid) == Process.getpgid(pid)
76
+
77
+ # If we get here, then oddly, SSH is not running in our process
78
+ # group and yet we got the signal. In this case, let's simply
79
+ # ignore it.
80
+ Process.kill(:SIGINT, pid)
81
+ rescue Errno::ESRCH
82
+ # This could happen if SSH exited after receiving the SIGINT,
83
+ # Ruby waited it, then ran our signal handler. In this case, we
84
+ # don't need to do anything, so we proceed.
85
+ end
86
+ end
87
+
88
+ begin
89
+ _, status = Process.wait2(pid)
90
+ return status.exited? ? status.exitstatus : 128 + status.termsig
91
+ ensure
92
+ Signal.trap('SIGINT', reset)
93
+ end
94
+ end
95
+
45
96
  def ensure_ssh_dir!
46
97
  FileUtils.mkdir_p(ssh_dir, mode: 0o700)
47
98
  end
@@ -23,7 +23,7 @@ module Aptible
23
23
  define_method 'config:add' do |*args|
24
24
  # FIXME: define_method - ?! Seriously, WTF Thor.
25
25
  app = ensure_app(options)
26
- env = Hash[args.map { |arg| arg.split('=', 2) }]
26
+ env = extract_env(args)
27
27
  operation = app.create_operation!(type: 'configure', env: env)
28
28
  puts 'Updating configuration and restarting app...'
29
29
  attach_to_operation_logs(operation)
@@ -40,7 +40,10 @@ module Aptible
40
40
  define_method 'config:rm' do |*args|
41
41
  # FIXME: define_method - ?! Seriously, WTF Thor.
42
42
  app = ensure_app(options)
43
- env = Hash[args.map { |arg| [arg, ''] }]
43
+ env = Hash[args.map do |arg|
44
+ validate_env_key!(arg)
45
+ [arg, '']
46
+ end]
44
47
  operation = app.create_operation!(type: 'configure', env: env)
45
48
  puts 'Updating configuration and restarting app...'
46
49
  attach_to_operation_logs(operation)
@@ -0,0 +1,89 @@
1
+ module Aptible
2
+ module CLI
3
+ module Subcommands
4
+ module Deploy
5
+ DOCKER_IMAGE_DEPLOY_ARGS = Hash[%w(
6
+ APTIBLE_DOCKER_IMAGE
7
+ APTIBLE_PRIVATE_REGISTRY_EMAIL
8
+ APTIBLE_PRIVATE_REGISTRY_USERNAME
9
+ APTIBLE_PRIVATE_REGISTRY_PASSWORD
10
+ ).map do |var|
11
+ opt = var.gsub(/^APTIBLE_/, '').downcase.to_sym
12
+ [opt, var]
13
+ end]
14
+
15
+ NULL_SHA1 = '0000000000000000000000000000000000000000'.freeze
16
+
17
+ def self.included(thor)
18
+ thor.class_eval do
19
+ include Helpers::Operation
20
+ include Helpers::App
21
+
22
+ desc 'deploy [OPTIONS] [VAR1=VAL1] [VAR=VAL2] ...', 'Deploy an app'
23
+ option :git_commitish,
24
+ desc: 'Deploy a specific git commit or branch: the ' \
25
+ 'commitish must have been pushed to Aptible beforehand'
26
+ option :git_detach,
27
+ type: :boolean, default: false,
28
+ desc: 'Detach this app from its git repository: ' \
29
+ 'its Procfile, Dockerfile, and .aptible.yml will be ' \
30
+ 'ignored until you deploy again with git'
31
+ DOCKER_IMAGE_DEPLOY_ARGS.each_pair do |opt, var|
32
+ option opt,
33
+ type: :string, banner: var,
34
+ desc: "Shorthand for #{var}=..."
35
+ end
36
+ app_options
37
+ def deploy(*args)
38
+ app = ensure_app(options)
39
+
40
+ git_ref = options[:git_commitish]
41
+ if options[:git_detach]
42
+ if git_ref
43
+ raise Thor::Error, 'The options --git-committish and ' \
44
+ '--git-detach are incompatible'
45
+ end
46
+ git_ref = NULL_SHA1
47
+ end
48
+
49
+ env = extract_env(args)
50
+
51
+ DOCKER_IMAGE_DEPLOY_ARGS.each_pair do |opt, var|
52
+ val = options[opt]
53
+ next unless val
54
+ if env[var] && env[var] != val
55
+ dasherized = "--#{opt.to_s.tr('_', '-')}"
56
+ raise Thor::Error, "The options #{dasherized} and #{var} " \
57
+ 'cannot be set to different values'
58
+ end
59
+ env[var] = val
60
+ end
61
+
62
+ opts = {
63
+ type: 'deploy',
64
+ env: env,
65
+ git_ref: git_ref
66
+ }.delete_if { |_, v| v.nil? || v.empty? }
67
+
68
+ allow_it = [
69
+ opts[:git_ref],
70
+ opts[:env].try(:[], 'APTIBLE_DOCKER_IMAGE'),
71
+ app.status == 'provisioned'
72
+ ].any? { |x| x }
73
+
74
+ unless allow_it
75
+ m = 'You need to deploy either from git or a Docker image'
76
+ raise Thor::Error, m
77
+ end
78
+
79
+ operation = app.create_operation!(opts)
80
+
81
+ puts 'Deploying app...'
82
+ attach_to_operation_logs(operation)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -34,7 +34,7 @@ module Aptible
34
34
  op = resource.create_operation!(type: 'logs', status: 'succeeded')
35
35
 
36
36
  ENV['ACCESS_TOKEN'] = fetch_token
37
- connect_to_ssh_portal(op, '-o', 'SendEnv=ACCESS_TOKEN', '-T')
37
+ exit_with_ssh_portal(op, '-o', 'SendEnv=ACCESS_TOKEN', '-T')
38
38
  end
39
39
  end
40
40
  end
@@ -20,7 +20,7 @@ module Aptible
20
20
 
21
21
  ENV['ACCESS_TOKEN'] = fetch_token
22
22
  opts = ['-o', 'SendEnv=ACCESS_TOKEN']
23
- connect_to_ssh_portal(op, *opts)
23
+ exit_with_ssh_portal(op, *opts)
24
24
  end
25
25
  end
26
26
  end
@@ -57,7 +57,7 @@ module Aptible
57
57
  end
58
58
  opts << tty_mode
59
59
 
60
- connect_to_ssh_portal(op, *opts)
60
+ exit_with_ssh_portal(op, *opts)
61
61
  end
62
62
 
63
63
  private
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module CLI
3
- VERSION = '0.10.0'.freeze
3
+ VERSION = '0.11.0'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ require 'open3'
3
+
4
+ USAGE = ARGV.fetch(0)
5
+
6
+ puts "Sync CLI usage in #{USAGE}"
7
+
8
+ txt, err, status = Open3.capture3(
9
+ { 'THOR_COLUMNS' => '1000' },
10
+ 'aptible', 'help'
11
+ )
12
+
13
+ raise "Failed to extract usage: #{err}" unless status.success?
14
+
15
+ usage = "```\n#{txt.gsub(/^$\n/, '')}```\n"
16
+
17
+ bits = []
18
+
19
+ File.open(USAGE) do |f|
20
+ in_usage = false
21
+
22
+ f.each_line do |l|
23
+ in_usage = false if l.include?('END USAGE')
24
+
25
+ bits << l unless in_usage
26
+
27
+ if l.include?('BEGIN USAGE')
28
+ in_usage = true
29
+ bits << usage
30
+ end
31
+ end
32
+ end
33
+
34
+ File.write(USAGE, bits.join(''))
@@ -89,4 +89,125 @@ describe Aptible::CLI::Helpers::Ssh do
89
89
  expect(has_yielded).to be_truthy
90
90
  end
91
91
  end
92
+
93
+ describe '#spawn_passthrough' do
94
+ let(:bins) { File.expand_path('../../../../script', __FILE__) }
95
+ let(:ruby) { Gem.win_platform? ? 'ruby.exe' : 'ruby' }
96
+ let(:wrapper) { [ruby, File.join(bins, 'ssh-spawn')] }
97
+ let(:exit_with) { [ruby, File.join(bins, 'exit-with')] }
98
+ let(:sigint) { [ruby, File.join(bins, 'pid-signal')] }
99
+ let(:setpgid) { [ruby, File.join(bins, 'setpgid')] }
100
+
101
+ let(:cleanup) { [] }
102
+
103
+ after do
104
+ cleanup.each do |pid|
105
+ begin
106
+ Process.kill(:SIGKILL, -pid)
107
+ rescue Errno::ESRCH, Errno::EINVAL
108
+ end
109
+ end
110
+ end
111
+
112
+ def spawn_with_cleanup(*args)
113
+ kw = Gem.win_platform? ? { new_pgroup: true } : { pgroup: true }
114
+ Process.spawn(*args, **kw).tap { |pid| cleanup << pid }
115
+ end
116
+
117
+ def wait_for_file(file)
118
+ 50.times do
119
+ return if File.exist?(file)
120
+ sleep 0.1
121
+ end
122
+
123
+ raise "File never showed up: #{file}"
124
+ end
125
+
126
+ def wait_for_pid(pid, timeout = 5)
127
+ (timeout * 10).times do
128
+ _, status = Process.wait2(pid, Process::WNOHANG)
129
+ return status if status
130
+ sleep 0.1
131
+ end
132
+
133
+ raise "PID never exited: #{pid}"
134
+ end
135
+
136
+ [0, 1].each do |c|
137
+ it "returns the command exit code (#{c})" do
138
+ pid = spawn_with_cleanup(*wrapper, *exit_with, c.to_s)
139
+ status = wait_for_pid(pid)
140
+ expect(status.exitstatus).to eq(c)
141
+ end
142
+ end
143
+
144
+ context 'signals' do
145
+ # Don't run these on Windows: sending SIGINT will send it to the entire
146
+ # console group, which includes the process running the specs.
147
+ before { skip 'Windows' if Gem.win_platform? }
148
+
149
+ it 'returns 128 + signal number when signalled' do
150
+ Dir.mktmpdir do |dir|
151
+ pid_file = File.join(dir, 'pid')
152
+ pid = spawn_with_cleanup(*wrapper, *sigint, pid_file)
153
+ wait_for_file(pid_file)
154
+
155
+ child_pid = Integer(File.read(pid_file).chomp)
156
+ Process.kill('INT', child_pid)
157
+
158
+ status = wait_for_pid(pid)
159
+
160
+ if Gem.win_platform?
161
+ expect(status.exitstatus).not_to eq(0)
162
+ else
163
+ expect(status.exitstatus).to eq(128 + Signal.list.fetch('INT'))
164
+ end
165
+ end
166
+ end
167
+
168
+ it 'does not proxy SIGINT when part of the same process group' do
169
+ Dir.mktmpdir do |dir|
170
+ pid_file = File.join(dir, 'pid')
171
+ pid = spawn_with_cleanup(*wrapper, *sigint, pid_file)
172
+ wait_for_file(pid_file)
173
+
174
+ child_pid = Integer(File.read(pid_file).chomp)
175
+ expect(Process.getpgid(child_pid)).to eq(Process.getpgid(pid))
176
+ Process.kill('INT', pid)
177
+
178
+ expect { wait_for_pid(pid, 2) }.to raise_error(/never exited/im)
179
+ end
180
+ end
181
+
182
+ it 'proxies SIGINT when process groups are different' do
183
+ Dir.mktmpdir do |dir|
184
+ pid_file = File.join(dir, 'pid')
185
+ pid = spawn_with_cleanup(*wrapper, *setpgid, *sigint, pid_file)
186
+ wait_for_file(pid_file)
187
+
188
+ child_pid = Integer(File.read(pid_file).chomp)
189
+ expect(Process.getpgid(child_pid)).not_to eq(Process.getpgid(pid))
190
+ Process.kill('INT', pid)
191
+
192
+ status = wait_for_pid(pid)
193
+ expect(status.exitstatus).to eq(128 + Signal.list.fetch('INT'))
194
+ end
195
+ end
196
+
197
+ it 'does not crash when receiving SIGINT concurrently' do
198
+ Dir.mktmpdir do |dir|
199
+ pid_file = File.join(dir, 'pid')
200
+ pid = spawn_with_cleanup(*wrapper, *sigint, pid_file)
201
+ wait_for_file(pid_file)
202
+
203
+ child_pid = Integer(File.read(pid_file).chomp)
204
+ expect(Process.getpgid(child_pid)).to eq(Process.getpgid(pid))
205
+ Process.kill('INT', -pid)
206
+
207
+ status = wait_for_pid(pid)
208
+ expect(status.exitstatus).to eq(128 + Signal.list.fetch('INT'))
209
+ end
210
+ end
211
+ end
212
+ end
92
213
  end
@@ -165,6 +165,34 @@ describe Aptible::CLI::Agent do
165
165
  end
166
166
  end
167
167
 
168
+ describe '#config:set' do
169
+ before do
170
+ allow(Aptible::Api::App).to receive(:all) { [app] }
171
+ allow(Aptible::Api::Account).to receive(:all) { [account] }
172
+ end
173
+
174
+ it 'should reject environment variables that start with -' do
175
+ allow(subject).to receive(:options) { { app: 'hello' } }
176
+
177
+ expect { subject.send('config:set', '-foo=bar') }
178
+ .to raise_error(/invalid argument/im)
179
+ end
180
+ end
181
+
182
+ describe '#config:rm' do
183
+ before do
184
+ allow(Aptible::Api::App).to receive(:all) { [app] }
185
+ allow(Aptible::Api::Account).to receive(:all) { [account] }
186
+ end
187
+
188
+ it 'should reject environment variables that start with -' do
189
+ allow(subject).to receive(:options) { { app: 'hello' } }
190
+
191
+ expect { subject.send('config:rm', '-foo') }
192
+ .to raise_error(/invalid argument/im)
193
+ end
194
+ end
195
+
168
196
  describe '#ensure_app' do
169
197
  it 'fails if no usable strategy is found' do
170
198
  strategies = [dummy_strategy_factory(nil, nil, false)]
@@ -0,0 +1,158 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aptible::CLI::Agent do
4
+ let!(:account) { Fabricate(:account, handle: 'foobar') }
5
+ let!(:app) { Fabricate(:app, handle: 'hello', account: account) }
6
+ let(:operation) { Fabricate(:operation) }
7
+
8
+ describe '#deploy' do
9
+ before do
10
+ allow(Aptible::Api::App).to receive(:all) { [app] }
11
+ allow(Aptible::Api::Account).to receive(:all) { [account] }
12
+ subject.stub(:fetch_token) { double 'token' }
13
+ end
14
+
15
+ context 'with app' do
16
+ let(:base_options) { { app: app.handle, environment: account.handle } }
17
+
18
+ def stub_options(**opts)
19
+ allow(subject).to receive(:options).and_return(base_options.merge(opts))
20
+ end
21
+
22
+ it 'deploys' do
23
+ stub_options
24
+
25
+ expect(app).to receive(:create_operation!)
26
+ .with(type: 'deploy').and_return(operation)
27
+ expect(subject).to receive(:attach_to_operation_logs)
28
+ .with(operation)
29
+
30
+ subject.deploy
31
+ end
32
+
33
+ it 'deploys a committish' do
34
+ stub_options(git_commitish: 'foobar')
35
+
36
+ expect(app).to receive(:create_operation!)
37
+ .with(type: 'deploy', git_ref: 'foobar').and_return(operation)
38
+ expect(subject).to receive(:attach_to_operation_logs)
39
+ .with(operation)
40
+
41
+ subject.deploy
42
+ end
43
+
44
+ it 'deploys a Docker image' do
45
+ stub_options(docker_image: 'foobar')
46
+
47
+ expect(app).to receive(:create_operation!)
48
+ .with(type: 'deploy', env: { 'APTIBLE_DOCKER_IMAGE' => 'foobar' })
49
+ .and_return(operation)
50
+ expect(subject).to receive(:attach_to_operation_logs)
51
+ .with(operation)
52
+
53
+ subject.deploy
54
+ end
55
+
56
+ it 'deploys with credentials' do
57
+ stub_options(
58
+ private_registry_email: 'foo',
59
+ private_registry_username: 'bar',
60
+ private_registry_password: 'qux'
61
+ )
62
+
63
+ env = {
64
+ 'APTIBLE_PRIVATE_REGISTRY_EMAIL' => 'foo',
65
+ 'APTIBLE_PRIVATE_REGISTRY_USERNAME' => 'bar',
66
+ 'APTIBLE_PRIVATE_REGISTRY_PASSWORD' => 'qux'
67
+ }
68
+
69
+ expect(app).to receive(:create_operation!)
70
+ .with(type: 'deploy', env: env)
71
+ .and_return(operation)
72
+ expect(subject).to receive(:attach_to_operation_logs)
73
+ .with(operation)
74
+
75
+ subject.deploy
76
+ end
77
+
78
+ it 'detaches a git repo' do
79
+ stub_options(git_detach: true)
80
+
81
+ ref = '0000000000000000000000000000000000000000'
82
+
83
+ expect(app).to receive(:create_operation!)
84
+ .with(type: 'deploy', git_ref: ref)
85
+ .and_return(operation)
86
+ expect(subject).to receive(:attach_to_operation_logs)
87
+ .with(operation)
88
+
89
+ subject.deploy
90
+ end
91
+
92
+ it 'fails if detaching the git repo and providing a commitish' do
93
+ stub_options(git_commitish: 'foo', git_detach: true)
94
+
95
+ expect { subject.deploy }.to raise_error(/are incompatible/im)
96
+ end
97
+
98
+ it 'allows setting configuration variables' do
99
+ stub_options
100
+
101
+ expect(app).to receive(:create_operation!)
102
+ .with(type: 'deploy', env: { 'FOO' => 'bar', 'BAR' => 'qux' })
103
+ .and_return(operation)
104
+ expect(subject).to receive(:attach_to_operation_logs)
105
+ .with(operation)
106
+
107
+ subject.deploy('FOO=bar', 'BAR=qux')
108
+ end
109
+
110
+ it 'allows unsetting configuration variables' do
111
+ stub_options
112
+
113
+ expect(app).to receive(:create_operation!)
114
+ .with(type: 'deploy', env: { 'FOO' => '' })
115
+ .and_return(operation)
116
+ expect(subject).to receive(:attach_to_operation_logs)
117
+ .with(operation)
118
+
119
+ subject.deploy('FOO=')
120
+ end
121
+
122
+ it 'rejects arguments with a leading -' do
123
+ stub_options
124
+
125
+ expect { subject.deploy('--aptible-docker-image=bar') }
126
+ .to raise_error(/invalid argument/im)
127
+ end
128
+
129
+ it 'allows redundant command line arguments' do
130
+ stub_options(docker_image: 'foobar')
131
+
132
+ expect(app).to receive(:create_operation!)
133
+ .with(type: 'deploy', env: { 'APTIBLE_DOCKER_IMAGE' => 'foobar' })
134
+ .and_return(operation)
135
+ expect(subject).to receive(:attach_to_operation_logs)
136
+ .with(operation)
137
+
138
+ subject.deploy('APTIBLE_DOCKER_IMAGE=foobar')
139
+ end
140
+
141
+ it 'reject contradictory command line argumnts' do
142
+ stub_options(docker_image: 'foobar')
143
+
144
+ expect { subject.deploy('APTIBLE_DOCKER_IMAGE=qux') }
145
+ .to raise_error(/different values/im)
146
+ end
147
+
148
+ it 'does not allow deploying nothing on an unprovisioned app' do
149
+ stub_options
150
+
151
+ app.stub(status: 'pending')
152
+
153
+ expect { subject.deploy }
154
+ .to raise_error(/either from git.*docker/im)
155
+ end
156
+ end
157
+ end
158
+ end
@@ -27,7 +27,7 @@ describe Aptible::CLI::Agent do
27
27
  expect(app).to receive(:create_operation!).with(
28
28
  type: 'logs', status: 'succeeded'
29
29
  ).and_return(op)
30
- expect(subject).to receive(:connect_to_ssh_portal).with(op, any_args)
30
+ expect(subject).to receive(:exit_with_ssh_portal).with(op, any_args)
31
31
  subject.send('logs')
32
32
  end
33
33
  end
@@ -47,7 +47,7 @@ describe Aptible::CLI::Agent do
47
47
  expect(database).to receive(:create_operation!).with(
48
48
  type: 'logs', status: 'succeeded'
49
49
  ).and_return(op)
50
- expect(subject).to receive(:connect_to_ssh_portal).with(op, any_args)
50
+ expect(subject).to receive(:exit_with_ssh_portal).with(op, any_args)
51
51
  subject.send('logs')
52
52
  end
53
53
  end
@@ -19,7 +19,7 @@ describe Aptible::CLI::Agent do
19
19
  allow(STDIN).to receive(:tty?).and_return(true)
20
20
  allow(STDOUT).to receive(:tty?).and_return(true)
21
21
 
22
- expect(subject).to receive(:connect_to_ssh_portal).with(
22
+ expect(subject).to receive(:exit_with_ssh_portal).with(
23
23
  operation, '-o', 'SendEnv=ACCESS_TOKEN', '-t'
24
24
  )
25
25
  subject.ssh
@@ -30,7 +30,7 @@ describe Aptible::CLI::Agent do
30
30
  allow(STDOUT).to receive(:tty?).and_return(true)
31
31
  allow(STDERR).to receive(:tty?).and_return(false)
32
32
 
33
- expect(subject).to receive(:connect_to_ssh_portal).with(
33
+ expect(subject).to receive(:exit_with_ssh_portal).with(
34
34
  operation, '-o', 'SendEnv=ACCESS_TOKEN', '-t'
35
35
  )
36
36
  subject.ssh
@@ -40,7 +40,7 @@ describe Aptible::CLI::Agent do
40
40
  allow(STDIN).to receive(:tty?).and_return(false)
41
41
  allow(STDOUT).to receive(:tty?).and_return(true)
42
42
 
43
- expect(subject).to receive(:connect_to_ssh_portal).with(
43
+ expect(subject).to receive(:exit_with_ssh_portal).with(
44
44
  operation, '-o', 'SendEnv=ACCESS_TOKEN', '-T'
45
45
  )
46
46
  subject.ssh
@@ -50,7 +50,7 @@ describe Aptible::CLI::Agent do
50
50
  allow(STDIN).to receive(:tty?).and_return(true)
51
51
  allow(STDOUT).to receive(:tty?).and_return(false)
52
52
 
53
- expect(subject).to receive(:connect_to_ssh_portal).with(
53
+ expect(subject).to receive(:exit_with_ssh_portal).with(
54
54
  operation, '-o', 'SendEnv=ACCESS_TOKEN', '-T'
55
55
  )
56
56
  subject.ssh
@@ -62,7 +62,7 @@ describe Aptible::CLI::Agent do
62
62
  allow(STDIN).to receive(:tty?).and_return(false)
63
63
  allow(STDOUT).to receive(:tty?).and_return(false)
64
64
 
65
- expect(subject).to receive(:connect_to_ssh_portal).with(
65
+ expect(subject).to receive(:exit_with_ssh_portal).with(
66
66
  operation, '-o', 'SendEnv=ACCESS_TOKEN', '-tt'
67
67
  )
68
68
  subject.ssh
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env ruby
2
+ exit Integer(ARGV.fetch(0))
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ File.write(ARGV.fetch(0), Process.pid.to_s)
3
+ Signal.trap('INT', 'SYSTEM_DEFAULT')
4
+ sleep 30
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ kw = Gem.win_platform? ? { new_pgroup: true } : { pgroup: true }
3
+ exec(*ARGV, **kw)
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'aptible/cli/helpers/ssh'
3
+ include Aptible::CLI::Helpers::Ssh
4
+ exit spawn_passthrough(ARGV)
metadata CHANGED
@@ -1,57 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aptible-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frank Macreery
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-16 00:00:00.000000000 Z
11
+ date: 2017-06-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: aptible-api
14
+ name: aptible-resource
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.9.20
19
+ version: 0.4.0
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.20
26
+ version: 0.4.0
27
27
  - !ruby/object:Gem::Dependency
28
- name: aptible-auth
28
+ name: aptible-api
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.11.12
33
+ version: 0.9.27
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.11.12
40
+ version: 0.9.27
41
41
  - !ruby/object:Gem::Dependency
42
- name: aptible-resource
42
+ name: aptible-auth
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 0.3.6
47
+ version: 0.11.13
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 0.3.6
54
+ version: 0.11.13
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: thor
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -238,6 +238,7 @@ files:
238
238
  - lib/aptible/cli/subcommands/backup.rb
239
239
  - lib/aptible/cli/subcommands/config.rb
240
240
  - lib/aptible/cli/subcommands/db.rb
241
+ - lib/aptible/cli/subcommands/deploy.rb
241
242
  - lib/aptible/cli/subcommands/domains.rb
242
243
  - lib/aptible/cli/subcommands/inspect.rb
243
244
  - lib/aptible/cli/subcommands/logs.rb
@@ -247,6 +248,7 @@ files:
247
248
  - lib/aptible/cli/subcommands/restart.rb
248
249
  - lib/aptible/cli/subcommands/ssh.rb
249
250
  - lib/aptible/cli/version.rb
251
+ - script/sync-readme-usage
250
252
  - spec/aptible/cli/agent_spec.rb
251
253
  - spec/aptible/cli/helpers/git_remote_handle_strategy_spec.rb
252
254
  - spec/aptible/cli/helpers/handle_from_git_remote_spec.rb
@@ -257,6 +259,7 @@ files:
257
259
  - spec/aptible/cli/subcommands/apps_spec.rb
258
260
  - spec/aptible/cli/subcommands/backup_spec.rb
259
261
  - spec/aptible/cli/subcommands/db_spec.rb
262
+ - spec/aptible/cli/subcommands/deploy_spec.rb
260
263
  - spec/aptible/cli/subcommands/domains_spec.rb
261
264
  - spec/aptible/cli/subcommands/inspect_spec.rb
262
265
  - spec/aptible/cli/subcommands/logs_spec.rb
@@ -276,6 +279,10 @@ files:
276
279
  - spec/mock/ssh
277
280
  - spec/mock/ssh.bat
278
281
  - spec/mock/ssh_mock.rb
282
+ - spec/script/exit-with
283
+ - spec/script/pid-signal
284
+ - spec/script/setpgid
285
+ - spec/script/ssh-spawn
279
286
  - spec/shared/mock_ssh_context.rb
280
287
  - spec/spec_helper.rb
281
288
  homepage: https://github.com/aptible/aptible-cli
@@ -313,6 +320,7 @@ test_files:
313
320
  - spec/aptible/cli/subcommands/apps_spec.rb
314
321
  - spec/aptible/cli/subcommands/backup_spec.rb
315
322
  - spec/aptible/cli/subcommands/db_spec.rb
323
+ - spec/aptible/cli/subcommands/deploy_spec.rb
316
324
  - spec/aptible/cli/subcommands/domains_spec.rb
317
325
  - spec/aptible/cli/subcommands/inspect_spec.rb
318
326
  - spec/aptible/cli/subcommands/logs_spec.rb
@@ -332,5 +340,9 @@ test_files:
332
340
  - spec/mock/ssh
333
341
  - spec/mock/ssh.bat
334
342
  - spec/mock/ssh_mock.rb
343
+ - spec/script/exit-with
344
+ - spec/script/pid-signal
345
+ - spec/script/setpgid
346
+ - spec/script/ssh-spawn
335
347
  - spec/shared/mock_ssh_context.rb
336
348
  - spec/spec_helper.rb