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 +4 -4
- data/.travis.yml +5 -0
- data/README.md +34 -30
- data/aptible-cli.gemspec +3 -3
- data/lib/aptible/cli/agent.rb +2 -0
- data/lib/aptible/cli/helpers/app.rb +15 -0
- data/lib/aptible/cli/helpers/operation.rb +5 -2
- data/lib/aptible/cli/helpers/ssh.rb +74 -23
- data/lib/aptible/cli/subcommands/config.rb +5 -2
- data/lib/aptible/cli/subcommands/deploy.rb +89 -0
- data/lib/aptible/cli/subcommands/logs.rb +1 -1
- data/lib/aptible/cli/subcommands/ps.rb +1 -1
- data/lib/aptible/cli/subcommands/ssh.rb +1 -1
- data/lib/aptible/cli/version.rb +1 -1
- data/script/sync-readme-usage +34 -0
- data/spec/aptible/cli/helpers/ssh_spec.rb +121 -0
- data/spec/aptible/cli/subcommands/apps_spec.rb +28 -0
- data/spec/aptible/cli/subcommands/deploy_spec.rb +158 -0
- data/spec/aptible/cli/subcommands/logs_spec.rb +2 -2
- data/spec/aptible/cli/subcommands/ssh_spec.rb +5 -5
- data/spec/script/exit-with +2 -0
- data/spec/script/pid-signal +4 -0
- data/spec/script/setpgid +3 -0
- data/spec/script/ssh-spawn +4 -0
- metadata +23 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4caf3814e0cffe6799a62625c5d9fb9f79dbd016
|
4
|
+
data.tar.gz: dbe1d0e306d35ce276c4ac0d323d0e1ea706aba4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1149e64d3194970dd488a36519eb07d1783d9ef860b587bd04f844de452afb848980ee4340e2e6a2cef0a10afa148812e48fdce7d9f18c296d072e52da5d45f0
|
7
|
+
data.tar.gz: 7aed71d453418b1d887d214974a6e60903338154c866d1a5ec90e59ab185f0ad02d0e06bc929046bc1290234572c3785c60fa5ac50c91498f9c6513f628a0127
|
data/.travis.yml
CHANGED
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
|
32
|
-
aptible apps:create HANDLE
|
33
|
-
aptible apps:deprovision
|
34
|
-
aptible apps:scale SERVICE [--container-count COUNT] [--container-size
|
35
|
-
aptible backup:list DB_HANDLE
|
36
|
-
aptible backup:restore [--handle HANDLE] [--size SIZE_GB]
|
37
|
-
aptible config
|
38
|
-
aptible config:add
|
39
|
-
aptible config:rm
|
40
|
-
aptible config:set
|
41
|
-
aptible config:unset
|
42
|
-
aptible db:backup HANDLE
|
43
|
-
aptible db:clone SOURCE DEST
|
44
|
-
aptible db:create HANDLE
|
45
|
-
aptible db:deprovision HANDLE
|
46
|
-
aptible db:dump HANDLE
|
47
|
-
aptible db:execute HANDLE SQL_FILE
|
48
|
-
aptible db:list
|
49
|
-
aptible db:reload HANDLE
|
50
|
-
aptible db:
|
51
|
-
aptible
|
52
|
-
aptible
|
53
|
-
aptible
|
54
|
-
aptible
|
55
|
-
aptible
|
56
|
-
aptible
|
57
|
-
aptible
|
58
|
-
aptible
|
59
|
-
aptible
|
60
|
-
aptible
|
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
|
|
data/aptible-cli.gemspec
CHANGED
@@ -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-
|
24
|
-
spec.add_dependency 'aptible-
|
25
|
-
spec.add_dependency 'aptible-
|
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'
|
data/lib/aptible/cli/agent.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
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
|
-
|
37
|
+
exit_with_ssh_portal(op, '-o', 'SendEnv=ACCESS_TOKEN', '-T')
|
38
38
|
end
|
39
39
|
end
|
40
40
|
end
|
data/lib/aptible/cli/version.rb
CHANGED
@@ -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(:
|
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(:
|
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(:
|
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(:
|
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(:
|
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(:
|
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(:
|
65
|
+
expect(subject).to receive(:exit_with_ssh_portal).with(
|
66
66
|
operation, '-o', 'SendEnv=ACCESS_TOKEN', '-tt'
|
67
67
|
)
|
68
68
|
subject.ssh
|
data/spec/script/setpgid
ADDED
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.
|
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-
|
11
|
+
date: 2017-06-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name: aptible-
|
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.
|
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.
|
26
|
+
version: 0.4.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name: aptible-
|
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.
|
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.
|
40
|
+
version: 0.9.27
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name: aptible-
|
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.
|
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.
|
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
|