aptible-cli 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 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